database (output)
Write records to PostgreSQL, MySQL, or SQLite.
The database output executes a SQL query for each record (or all of
them inside one transaction). Values are bound through the
parameters list — never spliced into the SQL text — with optional
transactional wrapping.
Minimal example
output:
type: database
connectionStringRef: ${WAREHOUSE_DATABASE_URL}
query: |
INSERT INTO orders (id, email, amount)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
amount = EXCLUDED.amount
parameters:
- record.id
- record.email
- record.amountOptions
| property | type | default | description |
|---|---|---|---|
typerequired | "database" | — | Module type discriminator. Must be `database` for this module. |
id | string | — | Unique identifier within the pipeline. |
name | string | — | Human-readable name. |
description | string | — | — |
enabled | boolean | true | Whether module is active. |
tags | array<string> | — | — |
onError | string | "fail" | Default error action. Case-insensitive; normalized to lowercase by the runtime. |
transaction | boolean | false | Wrap operations in a transaction. |
query | string | — | SQL query as a Jinja template rendered to text (control blocks {% if %}/{% for %} allowed). Dynamic values are never interpolated: use $1, $2, ... placeholders bound to the 'parameters' list. |
queryFile | string | — | Path to a SQL file containing the query template (same contract as 'query'). |
parameters | array<string> | — | Ordered expr expressions bound to the query's $N placeholders (parameters[0] -> $1). Evaluated against record/meta/state/pagination; values bind natively typed (nil -> NULL). |
connectionString | string | — | Database connection string (DSN). Avoid in production - use connectionStringRef instead. |
connectionStringRef | string | — | Environment variable reference for connection string. Format: ${ENV_VAR_NAME} |
driver | string | — | Database driver. Auto-detected from connection string if not specified. postgresmysqlsqlite |
maxOpenConns | integer | 10 | Maximum number of open connections in the pool. |
maxIdleConns | integer | 5 | Maximum number of idle connections in the pool. |
connMaxLifetimeSeconds | integer | 1800 | Maximum lifetime of a connection in seconds. |
connMaxIdleTimeSeconds | integer | 300 | Maximum idle time for a connection in seconds. |
timeoutMs | integer | 30000 | Query timeout in milliseconds. |
Query or query file
Provide one of query (inline) or queryFile (external). Inline is
convenient for short statements; external files are clearer for
anything beyond a one-liner.
queryFile: ./sql/upsert_order.sql
parameters:
- record.id
- record.email
- record.amount
- record.paid_atINSERT INTO orders (id, email, amount, paid_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
amount = EXCLUDED.amount,
paid_at = EXCLUDED.paid_at;Write $1, $2, … placeholders regardless of the database — Cannectors
translates them per driver (kept for PostgreSQL, ? for MySQL/SQLite).
Each parameters entry is an expression evaluated against the record
(parameters[0] → $1); values bind natively typed and a missing
field binds as NULL. See Templating.
Transactional writes
For atomic batch writes — either all records persist or none do — wrap in a transaction:
output:
type: database
connectionStringRef: ${WAREHOUSE_DATABASE_URL}
queryFile: ./sql/upsert_order.sql
transaction: trueThe runtime opens one transaction per output batch, executes the query once per record, then commits at the end. On any error, the transaction rolls back.
Don't combine transaction: true with onError: skip or onError: log
expecting partial success. On PostgreSQL the first failing statement
aborts the whole transaction (SQLSTATE 25P02); every later record then
fails with current transaction is aborted, the commit fails, and
nothing is persisted — not even the rows that inserted cleanly
before the error. Pick one: transaction: true with onError: fail
for all-or-nothing, or transaction: false with onError: skip to let
good records through and drop the bad ones.
Connection
Same as the database input — see
Connection for driver
auto-detection and connection-pool tuning. The output reuses the same
pool config (maxOpenConns, maxIdleConns, etc.).
The pool is opened before the input runs, not when the first record
arrives: a database you cannot reach fails the run before the source API
has been called and before any enrichment has been billed. The
connection string and the driver are resolved earlier still, when the
module is built, so a missing ${VAR} or an undetectable driver is
reported at startup.
Dry-run
cannectors run --dry-run prints the statement instead of executing it,
and opens no connection at all — a DSN pointing at a host you cannot
reach still produces a full preview:
📋 Dry-Run Preview (what would have been done):
Operation: INSERT INTO events (postgres)
Target: postgres://lab_user:[REDACTED]@warehouse.internal:5432/analytics
Records: 1000
Transaction: all statements in one transaction
Statement:
INSERT INTO events (id, seq) VALUES ($1, $2)
Parameters: record.id, record.seq
Values (truncated, use --verbose for all):
1: "VOL-00001", 1
2: "VOL-00002", 2
... (995 more executions)The password in the connection string is redacted unless
dryRunOptions.showCredentials is set; the bound values are record data
and are shown as they are, truncated for width unless you pass
--verbose. Parameters names what each value on a Values line binds
to, read off the rendered statement — so a conditional clause that was
left out, or a placeholder used twice, is labeled as it actually binds.
See dry-run mode for the full behaviour.
Examples
name: database-output-transaction-query-file
version: 1.0.0
description: Write records with a SQL query file in a transaction.
tags:
- database-output
- transaction
input:
type: httpPolling
schedule: "0 * * * *"
endpoint: https://source.example.com/api/products
dataField: products
filters:
- type: mapping
mappings:
- source: id
target: product_id
- source: sku
target: sku
- source: price
target: price
transforms:
- op: toFloat
- source: updated_at
target: updated_at
output:
type: database
connectionStringRef: ${WAREHOUSE_DATABASE_URL}
driver: postgres
queryFile: examples/assets/sql/upsert_product.sql
parameters:
- record.product_id
- record.sku
- record.price
- record.updated_at
transaction: true
onError: fail