Schema drift policies
The three policies#
When an incoming batch no longer matches the destination table, the pipeline's schema-drift policy decides what happens. Three policies exist, and the stored values are the ones in parentheses:
| Policy | Stored value | One-line behaviour |
|---|---|---|
| Reject | block_and_alert |
Stop the run, raise an alert, change nothing |
| Evolve | auto_add |
Add the new nullable columns and continue |
| Rescue | rescue |
Never stop — capture the unexpected data in a catch-all column |
Every pipeline that uses Evolve or Rescue also carries a fallback policy
for the cases the primary policy cannot handle. A fallback is always
block_and_alert or rescue — never auto_add.
Behaviour matrix#
| Drift kind | Reject | Evolve | Rescue |
|---|---|---|---|
| New column appears in the source | Blocks | ALTER TABLE … ADD COLUMN (nullable), run continues |
Value captured into the rescue column, run continues |
| Column type changed | Blocks | Defers to the fallback — Evolve never widens a type | Value captured into the rescue column, run continues |
| Column disappeared from the source | Blocks | Defers to the fallback — Evolve never drops | Run continues; the destination column stays and receives NULL |
| Column made nullable / not-null | Proceeds | Proceeds | Proceeds |
| Column order changed | Proceeds | Proceeds | Proceeds |
Non-structural drift — nullability changes and column reordering — always proceeds under every policy. The platform matches columns by name, not by position.
Evolve is additive only. The only DDL it will ever issue is a nullable
ADD COLUMN. It does not widen a type, does not tighten or relax a
constraint, and does not drop anything. Any non-additive change is handed to
the fallback policy.
Schema evolution needs an engine adapter, which exists only for postgres,
cockroachdb, mysql, mariadb and singlestore. On any other destination
Evolve has nothing to issue and the fallback applies.
The rescue column#
Rescue never blocks a run. Added fields and type-changed fields are written
into a reserved column, _abrq_rescued_data:
JSONBon the PostgreSQL family (postgres,cockroachdb)JSONon the MySQL family (mysql,singlestore,mariadb)
The payload is shaped:
{
"_schema_version": 3,
"_rescued_at": "2026-07-30T09:14:02Z",
"_reasons": ["unexpected_column"],
"fields": {
"customer_tier": "gold",
"signup_source": "referral"
}
}
Rows that did not drift leave _abrq_rescued_data NULL, so a
WHERE _abrq_rescued_data IS NOT NULL filter is an exact list of the affected
rows and costs nothing on the rest of the table.
Promoting a rescued field#
Rescue is a holding pattern, not a destination. When a rescued field turns out to be real, promote it:
- List the rescued rows for the pipeline and inspect the distinct field names in the payloads.
- Promote one field to a real column. In a single destination
transaction the platform issues the
ALTER TABLE … ADD COLUMN, back-fills the new column from every payload that carries the field, and strips that field out of the remaining payloads.
Properties of the promote operation:
- Forward-only. There is no demote. To undo, drop the column yourself.
- Idempotent. Re-running a promote that already completed is a no-op — the payloads no longer carry the field.
- Type-validated. The SQL type you promote to is checked against a whitelist; an arbitrary type expression is refused rather than interpolated.
Promote one field at a time. A payload with three unexpected fields takes three promotes, each with its own transaction.
CDC is different#
Change data capture mirrors an operator's table, so the rules are stricter.
Warning. Under CDC, destructive drift always blocks — under every policy, including Rescue. The run stops with:
destructive source schema change (…) — never auto-applied; resolve the mirror manually, then resume. Reconcile the mirror yourself, then resume the CDC table.
Under Rescue, non-destructive CDC drift is recorded as a drift event but no new schema version is created. The consequence is that the same fields are re-detected on the next sync and re-recorded — the pipeline keeps flowing, and the event count keeps rising until you either evolve the mirror or promote the field.
ETL uses a different vocabulary#
ETL tasks do not use the three-policy model. An ETL task's on_schema_drift
field takes block_and_alert or proceed only, and ETL never issues
ALTER TABLE against an operator-owned destination table under any setting.
See Load modes.
Defaults by pipeline family#
| Family | Default policy | Default fallback |
|---|---|---|
| CDC | block_and_alert |
block_and_alert |
| Streams | rescue |
block_and_alert |
| File ingestions | rescue |
block_and_alert |
| Email feeds | rescue |
block_and_alert |
| ETL | block_and_alert (two-value vocabulary) |
not applicable |
The reasoning is uniform: CDC mirrors an authoritative table and must never diverge silently, while the ingestion families take whatever an upstream system hands them and must not stop a feed over an extra field.
Alerts#
Every drift decision raises an alert:
- Severity
criticalwhen the run was blocked. - Severity
warningin every other case — evolved, rescued, proceeded.
Alerts are pushed to every enabled notification channel.
Note. Email feeds have no notification channel. Drift on an email feed is still recorded in the drift registry and still counted in the metrics, but nothing is delivered. Watch the metrics for that family.
Metrics#
Two counters cover the whole surface:
| Metric | Labels | Meaning |
|---|---|---|
abrq_schema_drift_events_total |
pipeline_type, action |
Drift events recorded, by family and by what was done. pipeline_type is one of cdc, stream, file_ingestion, email_feed; action is one of blocked, evolved, rescued, proceeded. |
abrq_rescued_rows_total |
pipeline_type |
Rows that captured unexpected columns into _abrq_rescued_data. |
A rising abrq_rescued_rows_total is the cue to promote a field: it means more
data is landing in the catch-all than in typed columns. A non-zero
rate(abrq_schema_drift_events_total{action="blocked"}[1h]) means a pipeline
is stopped and waiting for you.
See Metrics for the full list.