- All six target load modes run today;
cdc_mirrorapplies an op-column-driven change feed from the batch rows (see thecdc_mirrorrow and the CDC mirror details section below). - Pre/post hooks fire around the load.
- Dataset-level validators run as parameterized SQL probes.
- Row-level rules evaluate in memory, with optional DLQ quarantine.
Target load modes
Empty-source guard
WhenloadMode is truncate or blue_green and the upstream produced
0 rows, the runtime aborts the load before issuing the destructive
statement. The run fails with:
failOnEmptySource: false on the target node.
A side-effect: when an onMissAction: alert source comes up empty,
the source executor also emits a warning alert into the
System Health feed so operators see it in
the topbar widget without having to open the runs page.
Mode-switch validation
Picking a load mode in the UI surfaces inline errors before save when:upsertis chosen without anupsertKey, or with keys not in the destination schema, or with keys covering every column.incremental_watermarkis chosen without awatermarkColumn, or the column doesn’t exist in the destination schema.cdc_mirroris chosen without acdcOpColumn, or without at least one CDC key column inupsertKey, or with key columns not in the destination schema.truncateis chosen on a “full-refresh-shaped” pipeline (exactly one source + one target). Soft warning recommendsblue_green; you can keeptruncateif intentional.
parsePipelineIR enforces the presence preconditions server-side
(upsert → upsertKey, incremental_watermark → watermarkColumn,
cdc_mirror → cdcOpColumn + non-empty upsertKey) so the JSON IR tab can’t bypass them. The
richer schema-aware checks (keys/columns must exist in the destination,
truncate full-refresh warning) are editor-side; the “every column is
in upsertKey” case is additionally caught at load time by the SQL
builder, which refuses to emit a no-op DO NOTHING.
CDC mirror details
cdc_mirror treats the batch rows as a change feed instead of a plain
load. Two fields on the target node configure it:
cdcOpColumn— the source column carrying the per-row change op.upsertKey— the CDC key columns (the same fieldupsertmode uses). The target table needs a unique constraint or index on these columns for theON CONFLICTupsert path.
Semantics:
- Fail-closed on unknown ops — any other op value (including empty/NULL) fails the run before any DML touches the target. Silently skipping an unrecognized op would desynchronize the mirror.
- One transaction, upserts before deletes — a key that appears as both insert/update and delete in the same batch nets out deleted. Two upsert rows for the same key in one batch fail loudly (Postgres’s “cannot affect row a second time”) — one op per key per batch is the contract.
- The op column is control metadata — it is never written to the target, even when the target happens to carry a same-named column.
- Typed landing — the same column-aware NULLIF +
::castprojection as the other load modes. rowsLoadedreports the truthful applied count (upserts + deletes).
Blue-green details
Blue-green loads the new dataset into a sibling<table>_new, runs the
post-hook (e.g., a stats-collection command or a materialized-view
refresh), then issues three sequential alter table … rename
statements:
<table>_new fails, <table>_new is dropped and the
upstream error rethrows so the existing target is untouched.
Incremental-watermark details
The high-water mark lives in thepipeline_watermarks table keyed by
(tenantId, pipelineId, targetSchema, targetTable). Each run:
- Reads the persisted
watermarkValue(falls back toSELECT MAX(<watermarkColumn>) FROM <target>on the first run after an upgrade so we don’t replay history). - Computes the new MAX from the staging table.
- Wraps the
INSERT … WHERE col > $1and the watermarkupsertin a single Postgres transaction so a failed INSERT rolls back the watermark advance.
0001-01-01 (timestamp) or 0 (integer/bigint) so the first
non-empty run loads everything.
For timestamp watermark columns the persisted
watermarkValue is
stored in ISO-8601 form, so it round-trips cleanly through the
::TIMESTAMPTZ cast on the next run’s INSERT … WHERE col > $1. (Earlier
builds persisted a locale-formatted date string that Postgres rejected on
the second run, so a timestamp-watermark pipeline would fail after its
first successful load — now covered by a real-DB regression test.)Pre-load backup (optional)
AddbackupBeforeLoad to a destructive-mode target (truncate or
blue_green) to snapshot the current target rows before the load:
CREATE TABLE <schema>.<table>_backup_<runId> AS TABLE <schema>.<table>.
If that snapshot fails, the run aborts before the destructive
statement runs — better to fail than to run an unbacked destructive
load. After a successful load, backups beyond retainCount (default 3;
newest kept, ordered by creation time rather than name) are dropped. On
a failed load the backup table is left in place so you can recover
manually with INSERT INTO <table> SELECT * FROM <backup_table>. The
flag is ignored on non-destructive modes (append / upsert /
incremental_watermark).
Row-count anomaly detection (optional)
AddexpectedRowCountDelta to a target node to receive a warning
alert when today’s load is wildly outside the recent average:
windowRuns successful runs (default 7),
computes the running mean of rowsProcessed, and emits a warning
alert into the System Health feed if today’s load is outside the band.
The run still completes — anomaly detection never fails the run on its
own. Skipped when there are fewer than 3 prior successful runs.
Hooks
The IRpreHook and postHook slots accept four hook kinds. The
pre-hook runs before the load; the post-hook runs only on
successful load.
SQL hook safety
Hook SQL is admin-grade. The runtime forbids any statement starting with a data-modifying verb (drop, delete, update, truncate, insert,
merge, copy, grant, revoke, alter). Allowed verbs include
analyze, refresh, cluster, vacuum, set, explain, etc.
Embedded ; and SQL comments are also rejected.
Webhook safety
Webhook URLs flow throughlib/security/url-validator.validateAndResolve
— the same SSRF guard the rest of the platform uses. Private IP ranges
and DNS rebinding attempts are blocked before the fetch fires.
Dataset validators
{ ok, message?, observed? } result. The validate executor aggregates
them and applies each check’s onFail:
Every dataset check reports what it observed on the run trace under
meta.dataset.<check> as { ok, observed, message? }, whether or not it
passed. A configured check that leaves no trace entry did not run.
Row-level rules
upstream.meta.rows). If the upstream node produces no
in-memory rows (a pipeline shape that only materializes to staging),
row-level rules are skipped and a warning is recorded on the run
trace — they are never silently treated as passing. Each rule’s onFail:
fieldType.expected supports: string, integer, float, boolean,
date, timestamp, json, uuid.
DLQ (quarantine)
skip- or warn-mode rule is
written to a quarantine table. Note that a warn-mode row is written to
quarantine and continues downstream to the target — quarantine is a
record of what was flagged, not a diversion.
The table you configure is an input to the physical name, not the
name itself. The runtime derives dlq_<tenant-prefix>_<name>, dropping
any schema qualifier, so a pipeline author cannot create or write tables
elsewhere in the database. "table": "dlq.orders" in the tenant whose id
begins cdb967a5 writes to dlq_cdb967a5_orders. The run trace reports
the derived name as meta.quarantineTable — read that rather than
guessing from your config. The table is created idempotently on the first
failed-row write:
inspectFlowError tool reads this table; the future “Errors”
UI will render the same data row by row.
Honest residuals
custom_functionhook implementation — requires the plugin SDK.- Cross-load-mode transactions across hooks —
truncate,incremental_watermark,append,upsert, andcdc_mirrornow run their destructive + INSERT statements insideprisma.$transaction. Pre/post hooks still run in their own sessions, so a hook failing after a successful load won’t roll back the load.
Related
- Pipeline IR — the typed schema target/validate executors operate on.
- File Ingestion — Phase 3 source-side companion.
- MCP Flow Server —
inspectFlowErrorreads the DLQ table.