Write SQL queries for Celigo RDBMS exports and imports -- SELECT, INSERT, UPDATE, UPSERT, MERGE, delta, once, and bulk operations across Snowflake, Postgres, MySQL, SQL Server, Oracle, BigQuery, and Redshift. Use when editing rdbms.query or troubleshooting SQL errors.
RDBMS exports and imports use SQL queries to read from and write to relational databases. Queries are Handlebars templates -- the platform evaluates expressions like
{{{record.fieldName}}}
at runtime, then executes the resulting SQL against the connected database.
This skill covers what SQL to write and how to write it. For which adaptor type to use, connection setup, and queryType selection, see the related skills below.
Quick Reference
Context decision matrix
Where
Field
Handlebars prefix
Braces
Example
Export query
rdbms.query
record.
(parameterized) or none (standalone)
{{{ }}}
SELECT * FROM orders WHERE id = {{{record.id}}}
Export delta query
rdbms.query
platform-injected tokens
{{ }}
or
{{{ }}}
WHERE updated_at > '{{lastExportDateTime}}'
Export once mark-as-processed
rdbms.once.query
record.
{{{ }}}
UPDATE orders SET exported = true WHERE id = {{{record.id}}}
Import per_record query
rdbms.query[]
record.
{{{ }}}
INSERT INTO users (name) VALUES ('{{{record.name}}}')
# Test an export query -- invoke returns real dataceligo exports invoke <exportId># Test an import -- submit test recordsecho'[{"name":"test","email":"test@example.com"}]'| celigo imports invoke <importId>
Export Query Patterns
Standard SELECT
sql
SELECT id, name, email,statusFROM customers
WHEREstatus='ACTIVE'ORDERBY id
Delta export (incremental)
Use
{{lastExportDateTime}}
(platform-injected, not from a record). The token resolves to the timestamp of the last successful export run.
sql
SELECT id, name, email, updated_at
FROM customers
WHERE updated_at >'{{lastExportDateTime}}'ORDERBY updated_at ASC
For databases that need specific timestamp formats:
{"type":"once","rdbms":{"query":"SELECT id, name, email FROM orders WHERE exported = false","once":{"query":"UPDATE orders SET exported = true WHERE id = {{{record.id}}}"}}}
JOINs
sql
SELECT o.id, o.order_date, c.name AS customer_name, c.email, p.product_name, oi.quantity, oi.unit_price
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.status='SHIPPED'AND o.order_date >'{{lastExportDateTime}}'
Aggregations
sql
SELECT customer_id,COUNT(*)AS order_count,SUM(total)AS lifetime_value
FROM orders
WHEREstatusIN('COMPLETED','SHIPPED')GROUPBY customer_id
HAVINGSUM(total)>1000
Import Query Patterns
All import queries use
{{{record.fieldName}}}
to inject values from incoming records. The
query
field is an array of strings.
INSERT (per_record)
json
{"rdbms":{"queryType":["per_record"],"query":["INSERT INTO customers (name, email, phone) VALUES ('{{{record.name}}}', '{{{record.email}}}', '{{{record.phone}}}')"]}}
UPDATE (per_record)
json
{"rdbms":{"queryType":["per_record"],"query":["UPDATE customers SET name = '{{{record.name}}}', email = '{{{record.email}}}' WHERE id = {{{record.id}}}"]}}
UPSERT -- database-specific
MySQL (ON DUPLICATE KEY UPDATE):
json
{"rdbms":{"queryType":["per_record"],"query":["INSERT INTO customers (id, name, email) VALUES ({{{record.id}}}, '{{{record.name}}}', '{{{record.email}}}') ON DUPLICATE KEY UPDATE name = '{{{record.name}}}', email = '{{{record.email}}}'"]}}
PostgreSQL (ON CONFLICT):
json
{"rdbms":{"queryType":["per_record"],"query":["INSERT INTO customers (id, name, email) VALUES ({{{record.id}}}, '{{{record.name}}}', '{{{record.email}}}') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email"]}}
Snowflake (MERGE):
json
{"rdbms":{"queryType":["per_record"],"query":["MERGE INTO customers AS t USING (SELECT {{{record.id}}} AS id, '{{{record.name}}}' AS name, '{{{record.email}}}' AS email) AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name, t.email = s.email WHEN NOT MATCHED THEN INSERT (id, name, email) VALUES (s.id, s.name, s.email)"]}}
SQL Server (MERGE):
json
{"rdbms":{"queryType":["per_record"],"query":["MERGE INTO customers AS t USING (SELECT {{{record.id}}} AS id, '{{{record.name}}}' AS name, '{{{record.email}}}' AS email) AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name, t.email = s.email WHEN NOT MATCHED THEN INSERT (id, name, email) VALUES (s.id, s.name, s.email);"]}}
Multiple statements (per_record)
When you need to run multiple SQL statements per record, add them as separate array elements:
json
{"rdbms":{"queryType":["per_record"],"query":["INSERT INTO orders (id, customer_id, total) VALUES ({{{record.orderId}}}, {{{record.customerId}}}, {{{record.total}}})","UPDATE customers SET last_order_date = CURRENT_TIMESTAMP WHERE id = {{{record.customerId}}}"]}}
MERGEINTO WAREHOUSE.PUBLIC.CUSTOMERS AS t
USING {{import.rdbms.bulkLoad.preMergeTemporaryTable}} AS s
ON t.id = s.id
WHENMATCHEDAND s.updated_at > t.updated_at THENUPDATESET t.name = s.name, t.email = s.email, t.updated_at = s.updated_at
WHENNOTMATCHEDTHENINSERT(id, name, email, updated_at)VALUES(s.id, s.name, s.email, s.updated_at)
per_page batch operations
per_page
gives you the entire page of records as
batch_of_records
. Use
{{#each}}
to iterate:
json
{"rdbms":{"queryType":["per_page"],"query":["INSERT INTO customers (name, email) VALUES {{#each batch_of_records}}('{{{record.name}}}', '{{{record.email}}}'){{#unless @last}},{{/unless}}{{/each}}"]}}
Dialect Patterns
Snowflake
sql
-- Fully qualified table names (required unless connection sets default schema)SELECT*FROM MY_DATABASE.MY_SCHEMA.MY_TABLE
-- FLATTEN for semi-structured data (VARIANT columns)SELECT f.value:name::STRING AS name, f.value:email::STRING AS email
FROM MY_TABLE, LATERAL FLATTEN(input => MY_TABLE.json_column) f
-- QUALIFY for window function filteringSELECT*FROM orders
QUALIFY ROW_NUMBER()OVER(PARTITIONBY customer_id ORDERBY order_date DESC)=1-- Timestamp handlingWHERE updated_at > TO_TIMESTAMP('{{lastExportDateTime}}','YYYY-MM-DD"T"HH24:MI:SS')-- Case: Snowflake uppercases unquoted identifiers. Use double quotes to preserve caseSELECT"camelCaseColumn"FROM"MixedCaseTable"
PostgreSQL
sql
-- JSONB operatorsSELECTdata->>'name'AS name,data->'address'->>'city'AS city
FROM customers WHEREdata @>'{"active": true}'-- ILIKE for case-insensitive matchingSELECT*FROM products WHERE name ILIKE'%widget%'-- ON CONFLICT for upsertINSERTINTO customers (id, name, email)VALUES({{{record.id}}},'{{{record.name}}}','{{{record.email}}}')ON CONFLICT (id)DOUPDATESET name = EXCLUDED.name, email = EXCLUDED.email
-- Array operationsSELECT*FROM users WHERE'admin'=ANY(roles)-- CTEsWITH recent_orders AS(SELECT*FROM orders WHERE order_date >'{{lastExportDateTime}}')SELECT c.name, r.total FROM customers c JOIN recent_orders r ON c.id = r.customer_id
MySQL / MariaDB
sql
-- ON DUPLICATE KEY UPDATE for upsertINSERTINTO customers (id, name, email)VALUES({{{record.id}}},'{{{record.name}}}','{{{record.email}}}')ONDUPLICATEKEYUPDATE name =VALUES(name), email =VALUES(email)-- JSON_EXTRACT for JSON columnsSELECT JSON_EXTRACT(data,'$.name')AS name FROM customers
-- GROUP_CONCAT for string aggregationSELECT customer_id, GROUP_CONCAT(product_name SEPARATOR ', ')AS products
FROM order_items GROUPBY customer_id
-- IFNULL for null handlingSELECT IFNULL(middle_name,'')AS middle_name FROM users
-- Timestamp formattingWHERE updated_at > STR_TO_DATE('{{lastExportDateTime}}','%Y-%m-%d %H:%i:%s')
SQL Server / Azure Synapse
sql
-- MERGE with required semicolon terminatorMERGEINTO customers AS t
USING(SELECT {{{record.id}}} AS id,'{{{record.name}}}'AS name)AS s
ON t.id = s.id
WHENMATCHEDTHENUPDATESET t.name = s.name
WHENNOTMATCHEDTHENINSERT(id, name)VALUES(s.id, s.name);-- TOP instead of LIMITSELECTTOP100*FROM orders ORDERBY order_date DESC-- STRING_AGG for string aggregation (SQL Server 2017+)SELECT customer_id, STRING_AGG(product_name,', ')AS products
FROM order_items GROUPBY customer_id
-- CROSS APPLY for row-valued functionsSELECT c.name, o.total FROM customers c
CROSSAPPLY(SELECTTOP1*FROM orders WHERE customer_id = c.id ORDERBY order_date DESC) o
-- Bracket identifiers for reserved words or special charactersSELECT[order],[name]FROM[my-table]
Oracle
sql
-- NVL for null handlingSELECT NVL(middle_name,'')AS middle_name FROM users
-- ROWNUM for limiting results (pre-12c)SELECT*FROM(SELECT*FROM orders ORDERBY order_date DESC)WHERE ROWNUM <=100-- FETCH FIRST for limiting results (12c+)SELECT*FROM orders ORDERBY order_date DESCFETCHFIRST100ROWS ONLY
-- LISTAGG for string aggregationSELECT customer_id, LISTAGG(product_name,', ')WITHINGROUP(ORDERBY product_name)FROM order_items GROUPBY customer_id
-- MERGEMERGEINTO customers t USING(SELECT {{{record.id}}} AS id,'{{{record.name}}}'AS name FROM dual) s
ON(t.id = s.id)WHENMATCHEDTHENUPDATESET t.name = s.name
WHENNOTMATCHEDTHENINSERT(id, name)VALUES(s.id, s.name)
BigQuery
sql
-- Backtick identifiersSELECT*FROM`project.dataset.table`-- UNNEST for array columnsSELECT*FROM`orders`, UNNEST(items)AS item
-- STRUCT accessSELECT address.city, address.state FROM customers
-- SAFE_DIVIDE to avoid division by zeroSELECT SAFE_DIVIDE(revenue, orders)AS avg_order_value FROM metrics
-- Timestamp handlingWHERE updated_at >TIMESTAMP('{{lastExportDateTime}}')
Redshift
sql
-- PostgreSQL-based syntaxSELECT*FROM orders WHEREstatus='ACTIVE'-- LISTAGG for string aggregationSELECT customer_id, LISTAGG(product_name,', ')WITHINGROUP(ORDERBY product_name)FROM order_items GROUPBY customer_id
-- COPY-oriented -- bulk_load uses Redshift COPY under the hood-- For custom merge logic, use overrideMergeQuery with staging table reference
<!-- TIER:3 -->
Pre-Submit Checklist
Export queries
SELECT query is syntactically valid for the target database dialect
Delta uses
{{lastExportDateTime}}
in the query, NOT a
delta
object inside
rdbms
Once export has both queries --
rdbms.query
for SELECT and
rdbms.once.query
for UPDATE
Table and column names exist -- verify with
celigo metadata types/fields <connectionId>
Snowflake uses fully qualified names --
database.schema.table
unless the connection sets defaults
Import queries
query
is an array of strings --
["INSERT INTO ..."]
, not
"INSERT INTO ..."
queryType
matches the operation --
["per_record"]
for UPDATE/UPSERT,
["bulk_insert"]
for pure INSERT,
["bulk_load"]
for high-volume
All
{{{record.fieldName}}}
paths match incoming data -- invoke the upstream export to verify field names
Strings are quoted, numbers are not --
'{{{record.name}}}'
vs
{{{record.id}}}
Uses
record.
prefix -- not bare
fieldName
or
data.fieldName
Uses triple braces
{{{ }}}
-- double braces auto-wrap in quotes, breaking numeric values and SQL syntax
bulkInsert/bulkLoad not set alongside query -- these are mutually exclusive with the
query
field (except
overrideMergeQuery
)
Cross-resource
Connection type is RDBMS --
type
on the connection matches one of: mysql, mariadb, postgresql, mssql, azuresynapse, oracle, snowflake, bigquery, redshift
SQL dialect matches the database -- MERGE syntax differs between Snowflake, SQL Server, Oracle, PostgreSQL
Gotchas
Double braces auto-format in RDBMS.
{{record.name}}
outputs
'value'
(wrapped in quotes).
{{{record.name}}}
outputs
value
(raw). Use triple braces and add your own quotes for strings -- this gives you control and avoids double-quoting or broken numeric values.
Import
query
must be an array.
"query": "INSERT INTO ..."
fails silently or throws a Cast error. Always use
["INSERT INTO ..."]
.
queryType
values are specific. Use
["per_record"]
,
["bulk_insert"]
,
["per_page"]
,
["bulk_load"]
. Do NOT use
["INSERT"]
or
["UPDATE"]
as standalone values.
Snowflake rejects legacy
queryType
values on PUT.
insert
/
update
may work on POST but fail on PUT. Use
per_record
or
bulk_insert
from the start.
Missing
record.
prefix produces empty values.
{{{name}}}
resolves to nothing. Always use
{{{record.name}}}
.
Snowflake requires fully qualified table names.
database.schema.table
unless the connection sets a default schema. Unqualified names fail silently or hit the wrong table.
per_page
has a different Handlebars context. The context is