A dbt contract that blocks a bad batch before it reaches the mart. The sabotaged batch loads without a single error and reports revenue of $4,905,051 instead of $395,751 — a 1,139% overstatement that a plain pipeline waves straight through. The contract fails 12 tests and the mart is never built.
Public · synthetic demoEvery one of the twelve defects in the sabotaged batch is a defect that survives a plain load. Nothing throws. A duplicated customer file, an order amount that arrived in cents, a status with a capital D — the rows land, the job exits 0, and the number a stakeholder reads is wrong by an order of magnitude.
| run | rows loaded | revenue reported | errors | tests failed |
|---|---|---|---|---|
| Plain load — clean batch | 900 | $395,751.28 | 0 | — |
| Plain load — sabotaged | 901 | $4,905,051.18 | 0 | — |
| Contract — clean batch | 900 | $395,751.28 | 0 | 0 of 15 |
| Contract — sabotaged | blocked | not built | 12 | 12 of 15 |
D9 tests the raw value, not the normalised one. It is tempting to lower() the status in staging and test the clean column — but then the test can never fail, and the downstream filter doing an exact match on 'delivered' still silently drops the row. The model exposes both status_raw and status_normalised; the contract tests the raw one.
D7 is a contract, not a bug. Nothing is wrong with an EUR order. What is wrong is that fct_revenue_daily sums amount without conversion, so mixing currencies makes the total meaningless. The single-currency rule is written down as a test precisely because the assumption lives in a model somewhere else.
| # | what arrived | caught by |
|---|---|---|
| D1 | customer_id 8 duplicated — a re-sent file loaded twice | unique |
| D2 | customer 16 email arrived as "", not NULL | not_null |
| D3 | country USA instead of ISO-2 US | accepted_values |
| D4 | signup_date in 2027 — an upstream timezone bug | not_in_future |
| D5 | order 101 references customer 99999, which does not exist | relationships |
| D6 | order 201 amount is -450.00 with status placed | non_negative |
| D7 | orders 301–302 switched to EUR while the mart sums as USD | accepted_values |
| D8 | order 401 amount 4,500,000 — a cents/dollars mix-up | within_magnitude |
| D9 | status Delivered with a capital D | accepted_values on raw |
| D10 | order 601 present twice — double revenue recognition | unique |
| D11 | order 701 dated before the reporting window opens | within_reporting_window |
| D12 | order 801 amount arrived empty and loads as 0 | not_null |
pip install dbt-core dbt-duckdb python3 scripts/make_batches.py # regenerates both batches, seed is fixed ./scripts/run_evidence.sh # runs the contract over each, writes evidence/ python3 scripts/naive_vs_gate.py # shows what a plain load reports instead
The warehouse here is DuckDB so the whole thing runs on a laptop in under a second. The contract is ordinary dbt — moving it to Snowflake, BigQuery or Postgres is a change of profiles.yml, not a change of tests.
· Synthetic batches (900 rows, fixed seed) — a demonstrator of the method, not a benchmark. · Contracts stop bad data. They do not tell you a job never ran, or that a scraper returned an empty page — those are pipeline-heartbeat and scraper-canary. · The twelve defects are the ones that survive a plain load. Defects that crash the load are already visible and are deliberately out of scope.