cannectors

sql_call

Per-record SQL enrichment with caching and merge strategies.

The sql_call filter runs a SQL query per record, then merges the result back onto the record. The query text carries $1, $2, … placeholders and the parameters list supplies the values — they are bound by the driver, never string-spliced into the SQL, so they're safe by construction.

Minimal example

filters:
  - type: sql_call
    connectionStringRef: ${REFERENCE_DATABASE_URL}
    query: SELECT tier FROM customers WHERE id = $1
    parameters:
      - record.customerId
    mergeStrategy: merge

Options

propertytypedefaultdescription
typerequired
"sql_call"Module type discriminator. Must be `sql_call` for this module.
id
stringUnique identifier within the pipeline.
name
stringHuman-readable name.
description
string
enabled
booleantrueWhether module is active.
tags
array<string>
onError
string"fail"Default error action. Case-insensitive; normalized to lowercase by the runtime.
mergeStrategy
string"merge"How to merge query results with input records.
mergereplaceappend
resultKey
stringKey for storing result in append mode.
object
query
stringSQL 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
stringPath 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
stringDatabase connection string (DSN). Avoid in production - use connectionStringRef instead.
connectionStringRef
stringEnvironment variable reference for connection string. Format: ${ENV_VAR_NAME}
driver
stringDatabase driver. Auto-detected from connection string if not specified.
postgresmysqlsqlite
maxOpenConns
integer10Maximum number of open connections in the pool.
maxIdleConns
integer5Maximum number of idle connections in the pool.
connMaxLifetimeSeconds
integer1800Maximum lifetime of a connection in seconds.
connMaxIdleTimeSeconds
integer300Maximum idle time for a connection in seconds.
timeoutMs
integer30000Query timeout in milliseconds.

Query and parameters

query is a Jinja template rendered to SQL text — {% if %} blocks can build optional clauses. Dynamic values are referenced with $1, $2, … placeholders, bound to the parameters list (1-based: parameters[0]$1). Each entry is an expr expression evaluated against the record; values bind natively typed and a missing field binds as NULL.

query: |
  SELECT plan, mrr
  FROM accounts
  WHERE id = $1
  {% if record.region %}AND region = $2{% endif %}
parameters:
  - record.account_id
  - record.region

Write $N regardless of the database — Cannectors translates placeholders per driver (kept for PostgreSQL, ? for MySQL/SQLite). A placeholder inside a skipped {% if %} branch is neither evaluated nor bound. cannectors validate rejects a $N with no matching parameter, and a parameter never referenced by the query.

cannectors validate rejects an output tag ({{ … }}) inside a SQL query — it would splice a raw, unescaped value into the SQL. Dynamic values always go through parameters. Control blocks ({% if %}) are allowed.

Connection

Same shape as the database input — connectionString or connectionStringRef. Use the ref form for production:

connectionStringRef: ${REFERENCE_DATABASE_URL}

See database input · connection for driver auto-detection and tuning.

Merge strategies

StrategyEffect
merge (default)Deep-merge the first returned row onto the record. Nested objects are merged; SQL values overwrite conflicts.
replaceOverlay the first returned row onto the record. Existing fields not present in the row are preserved.
appendStore the first returned row under resultKey. Requires resultKey to be set.
mergeStrategy: append
resultKey: enrichments

sql_call currently consumes only the first row returned by the query. For one-to-many enrichment, aggregate the related data in SQL so the query returns one row with the desired value under a column.

Caching

sql_call has the same cache config as http_call — LRU with TTL.

cache:
  enabled: true
  maxSize: 1000
  ttlSeconds: 600
  key: "{{ record.customerId }}"

The same warning applies: without an explicit cache.key or per-record parameters, all records will hit the same cache slot.

The cache also outlives a run the same way it does for http_call: on a scheduled pipeline, ttlSeconds is how stale a lookup may get, not a per-tick setting. Reference tables tolerate a high TTL; anything that changes during the day does not.

Examples

examples/17-sql-call-merge-cache.yamlview source ↗
17-sql-call-merge-cache.yaml
name: sql-call-merge-cache
version: 1.0.0
description: Enrich records with a SQL lookup and deep merge the result.
tags:
  - sql-call
  - cache
input:
  type: httpPolling
  schedule: "*/30 * * * *"
  endpoint: https://source.example.com/api/orders
  dataField: orders
filters:
  - type: sql_call
    connectionStringRef: ${REFERENCE_DATABASE_URL}
    driver: postgres
    query: |
      select segment, account_owner
      from customer_reference
      where customer_id = $1
      limit 1
    parameters:
      - record.customer.id
    mergeStrategy: merge
    cache:
      enabled: true
      maxSize: 2000
      ttlSeconds: 600
      key: "customer:{{record.customer.id}}"
output:
  type: httpRequest
  endpoint: https://destination.example.com/api/orders/enriched
  method: POST
  requestMode: batch
examples/18-sql-call-append-query-file.yamlview source ↗
18-sql-call-append-query-file.yaml
name: sql-call-append-query-file
version: 1.0.0
description: Use queryFile for SQL enrichment and append the lookup result under a custom key.
tags:
  - sql-call
  - query-file
input:
  type: httpPolling
  schedule: "0 * * * *"
  endpoint: https://source.example.com/api/customers
  dataField: customers
filters:
  - type: sql_call
    connectionStringRef: ${REFERENCE_DATABASE_URL}
    driver: postgres
    queryFile: examples/assets/sql/customer_lookup.sql
    parameters:
      - record.id
    mergeStrategy: append
    resultKey: reference
    cache:
      enabled: true
      maxSize: 1000
      ttlSeconds: 300
      key: "customer:{{record.id}}"
output:
  type: httpRequest
  endpoint: https://destination.example.com/api/customers/reference
  method: POST
  requestMode: batch

Cross-references