coconutlabs

gallery unit · data core

Schema-drift contract guardrail

The dashboard says revenue fell by half. It did not: an upstream feed switched cents to dollars, and every check on the pipeline still passes.

scenario sandbox

You are the analytics engineer. This morning's load is 240 rows of an ELT landing table, and it may carry one of these drifts. The real contract runs in your browser, computed live, not canned.

inject a drift

clean pass

The table as landed passes both checks. The report reads revenue $299,354.48 · 24 refunds · 3 currency buckets.

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

Baseline: the table as landed. Nothing should fire. 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.

what you are looking at

  1. 01 · data in

    A synthetic ELT landing table of payment rows loads in your browser. The drift you pick is injected into a copy of it.

  2. 02 · check runs

    The contract checks value domains, a magnitude band, and a precision signature. The standard schema / null / row-count check runs beside it.

  3. 03 · verdict out

    Both verdicts land side by side, plus the number the downstream report would emit.

Go deeper

Watch the walkthrough (45 s, silent)

Silent drifts at the door, caught before the warehouse · recorded from this page, unedited.

The measured result: 6/6 vs 1/6

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 read as dollars, still 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' re-encoded as 'TRUE', '1', or 'yes'refund count silently drops to 0missescatches
Type wideningint widened to 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.

Where the problem statement comes from

“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 full mechanism: 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 + how to reproduce

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.