Read the launch

gallery unit · data core · bottleneck class G

Schema-drift contract guardrail

“A crashed pipeline gets fixed. A lying one gets quoted in a board meeting.”

dlt (dlthub) engineering blog · “Schema evolution in data pipelines” · Aman Gupta · 2026-06-05

The drift keeps the column present and non-null. So the schema check passes, the null check passes, the row-count check passes — and the number goes quietly wrong.

Six drifts, one landing table

guardrail 6/6 · standard check 1/6 · ~0.1 ms/check

DriftWhat it does downstreamStd checkContract
Partial unit changecents→dollars, in rangerevenue reads $157,512 (true $299,354)missescatches
Precision truncationfloored to whole dollarssum drifts; values suspiciously roundmissescatches
New enum valueunannounced CADa currency bucket nobody handlesmissescatches
Boolean re-encoding'true' → 'TRUE'/'1'/'yes'refund count silently drops to 0missescatches
Type wideningint → text, coerced pastthe revenue sum can't runmissescatches
Out-of-range scalethe one loud driftvalues blow past the maxcatchescatches

The standard check catches exactly one drift — the only one whose values leave the observed range. Every other drift keeps each value individually valid and corrupts the aggregate. The unit-scale one makes the revenue report read $157,512 against a true $299,354: no error, no null, quoted in the meeting.

run it yourself

This runs the real guardrail in your browser on 240 rows of a synthetic ELT landing table. Inject a drift and watch the contract catch what the standard schema / null / row-count check misses — and what it does to the number the report emits. Computed live, not canned.

Baseline: the table as landed. Nothing should fire.

The contract guardrailpassed

no violations — clean data passes

Standard schema / null / row-count checkpassed

passed — clean data is valid

Downstream reportbaseline

revenue $299,354.48 · 24 refunds · 3 currency buckets

Same contract and standard check as the Python harness; verified to reproduce its 6/6-vs-1/6 verdicts on this table. The one drift the standard check catches is the only one that leaves the observed range.

Why the usual gate fails

  • A schema check reads the shape. Column present, dtype unchanged, non-null, row count identical, values inside the observed range. Every silent drift preserves all five. The inferred schema signs off.
  • The corruption is in meaning, not shape. A currency that split into two spellings, a boolean that grew a third encoding, a cents column that started shipping dollars — each value still looks valid. Only the aggregate is wrong.

The contract pins meaning: the declared dtype, the allowed value domain of each categorical column, a magnitude band for the money column, and a ceiling on how “round” those values may be. A drift that survives shape checks still has to survive the domain, the band, and the precision signature — and these don’t.

standard check : present? non-null? same dtype? in [min,max]? same rowcount?   → all pass
contract       : currency ∈ {USD,EUR,GBP}?  amount mean within ±20%?  ≤10% whole-dollar?
                 → new "CAD", or a mean off by 47%, or 30% suddenly round → FLAGGED

Evidence

Tier 4 — one landing table, six drifts, scored by the contract guardrail and by a real off-the-shelf control (pandera’s inferred schema at defaults, plus explicit row-count and null-ratio checks). The control is not a strawman: it catches the one drift that leaves the observed range, and only that one. The rest it waves through, and the downstream number moves. Synthetic data (drift needs controlled before/after); stated, not hidden.

make setup && make test && make run   # $0, laptop, no GPU, no network

Honest gaps

The contract’s categorical domains are declared, so a genuinely new-but-legitimate category reads as a drift until the contract is updated — that friction is the point, but it is friction. The magnitude and precision bands are fitted from a golden sample and tuned for this table; production needs per-column tuning. And the hardest drift class is out of reach of any per-column contract: a cross-field one — say, amounts that stop being normalized to a common currency while every column stays in its own domain — corrupts the sum with no new value anywhere, and needs a reconciliation total, not a schema. That is the next guardrail, not this one.