Four Ways a dbt Project Passes Every Test While Source Data Drifts
November 5, 2026 · 6 min read
A green dbt run feels like proof. Picture one where the models built, the not_null and unique tests passed, the freshness check came back inside its window, and the dashboard refreshed on schedule. Then someone in finance asks why EMEA revenue is eleven percent below what the ERP shows, and nobody can find the failure, because nothing failed.
dbt is very good at its job. It tests the transformations you wrote against the assumptions you wrote down. What it cannot do on its own is notice that the source system changed underneath those assumptions. Here are four ways that plays out, and why each one stays green.
What dbt Actually Checks
The built-in generic tests (unique, not_null, accepted_values, relationships) check that data in the warehouse satisfies a rule at the moment the test runs. Source freshness checks the newest value of a loaded_at column against a threshold you set. Both are useful. Neither compares what a column means today with what it meant last month. They answer "did data arrive, and does it fit my rules," not "is this still the same data."
1. Freshness Passes Because Something Landed
Source freshness looks at the maximum timestamp in a table. If a vendor release changes a status field so that "closed" now covers both closed and on hold, rows keep arriving on schedule and the freshness check stays green. The same happens when a sync lands only part of a batch. The newest row is recent, so the table looks healthy while a third of the records are missing.
2. accepted_values Passes Because the New Value Fits
An accepted_values test is good at catching a value nobody expected. It is blind to an expected value that now means something else. Amounts that moved from dollars to cents are still numeric and still not null. A region code that was reorganized after an acquisition is still one of the allowed codes. A discount field that used to hold a percentage and now holds a rate between zero and one passes every test you have, and your margin report is quietly wrong.
3. The Column Was Never in a Test
Tests cover what someone thought to cover. A source table might have sixty columns while a staging model selects eight, and the other fifty-two are not watched at all. A renamed column usually fails loudly, which is the good case. The bad cases are quieter: a new column starts carrying data that used to live in an old one, an upstream view begins returning a default value in place of a dropped field, or a nullable column that was always populated starts arriving empty for one customer segment. If the model never selected it, or only selects it behind a coalesce, nothing complains.
4. The Test Runs on a Copy, Not on the Source
By the time dbt runs, an ingestion tool has already landed the data. Managed connectors commonly handle upstream schema changes by adding columns or widening types automatically, which is convenient and also means the warehouse copy changes shape without anyone being told. Business logic that lives in the source application, such as a pricing rule or a workflow state machine, never reaches the warehouse as a testable object at all. dbt can only judge what survived the trip.
A Quick Example
Picture an order feed from a billing platform into your warehouse. The platform ships a release that changes the invoice total field from a gross amount to a net amount after tax credits. The column name is the same, the type is the same, and every row still arrives within the hour. Your staging model passes not_null, your freshness check passes, and your relationships test passes. Revenue in the warehouse now runs a few percent low, and the gap grows with every customer who has credits. It surfaces three weeks later, during close, as a reconciliation variance nobody can explain.
What Teams Add, and Where It Stops
Most mature dbt teams respond with more tooling around the project. Common additions include:
- Schema change and volume anomaly tests from packages such as elementary, which catch added, removed, or retyped columns and sudden row count swings.
- Value distribution tests from dbt_expectations, which flag when a column's range or share of nulls moves outside a learned band.
- Contracts and model versions in dbt itself, which protect downstream consumers from changes you make, though not from changes upstream.
For the contracts and registry side of this, see where each one stops working.
These help, and they are worth having. They share three limits. They run after load, on a schedule, so the bad data is already in the warehouse and often already in a dashboard. They learn what a distribution looks like without knowing why it moved, so a legitimate seasonal shift and a unit change look the same. And they sit downstream of the systems that actually changed, which makes the investigation slow. The same pattern shows up in CDC pipelines, where a column can change meaning without changing shape.
Where the Check Has to Sit
Meaning drifts at the source, so that is where it needs to be watched. That means looking at the values in the source tables, not just their structure: units, code sets, relationships between fields, and counts in context. It also means doing it on every monitoring cycle rather than at the next scheduled run. A dbt project is one consumer of that data. Finance exports, reverse ETL syncs, and AI agents are others, and none of them run your tests.
The Practical Takeaway
Keep your dbt tests and keep adding to them. Then ask a different question of your most important sources: if the vendor changed what a field means tomorrow, which of our checks would notice, and how long would it take? If the honest answer is "the quarterly reconciliation," there is a gap between your green checkmarks and your data.
mmune runs as a read-only overlay across the systems feeding your warehouse, watching for the semantic drift that tests and freshness checks were never built to see. Find your blind spots before the next green run turns out to be wrong.