whatbreaks
Know what a dbt change breaks — before you merge it.
Column-level blast radius in CI. No warehouse, no secrets, no backend.
Real CLI output. CI regenerates this image and fails if it has drifted.
The problem
You remove a column. dbt build passes. Three hops downstream a dashboard breaks, and you find out
a week later from an angry Slack message.
dbt ls --select model+ knows which models are downstream. It does not know which columns
are — so a change to one column flags every model beneath it, whether or not that column reaches
them. Technically correct, too coarse to act on.
Both models are downstream. Only one selects the column — the other breaks because it filters on it, which never shows up in the output schema at all. whatbreaks reports both, and says which is which.
Why not the existing tools
Column-level lineage for dbt is not new. sqlglot does the parsing and several good tools build on
it. The gap is that every existing CI impact tool needs infrastructure:
| Tool | Requires |
|---|---|
| Recce | live warehouse + two dbt environments |
| dbt-column-lineage | catalog.json, i.e. a warehouse |
| SQLMesh | adopting SQLMesh as your framework |
| dbt Cloud Explorer | dbt Cloud, Enterprise tier |
| Datafold · Atlan · Sifflet · Metaplane | a hosted catalog (SaaS) |
| whatbreaks | dbt parse output. Nothing else. |
Because it needs no credentials, it can run on pull requests from forks — where GITHUB_TOKEN
is read-only and secrets are withheld.
If you already run Recce or Datafold, they answer a different and often better question — "did the data actually change?" — by querying your warehouse. whatbreaks answers "what could break?" statically, in seconds, for free.
Try it
pip install whatbreaks # or: uv tool install whatbreaks
Four runtime dependencies. No warehouse driver, no service, no account.
The repo ships a runnable example — no warehouse, no dbt install:
git clone https://github.com/Ridadata/whatbreaks && cd whatbreaks
whatbreaks check \
--base examples/quickstart/base/target/manifest.json \
--head examples/quickstart/head/target/manifest.json
That is the run in the demo above. See examples/quickstart for what it
models.
Use it on your project
You need two manifests — one from the base commit, one from your change. dbt parse produces them
offline, with no warehouse connection:
git worktree add ../base origin/main
(cd ../base && dbt parse) # base
dbt parse # your change
whatbreaks check --base ../base/target/manifest.json --head target/manifest.json
| Flag | |
|---|---|
--fail-on breaking |
default — only confirmed breakage fails the run |
--fail-on possibly-breaking |
stricter |
--fail-on never |
report only, always exit 0 |
--format text | json | markdown |
humans · tooling · PR comments |
Exit codes — 0 clean · 1 findings at or above the threshold · 2 bad input (nothing analysed).
In CI
- uses: Ridadata/whatbreaks/action@v0.2.0
with:
base-manifest: .base/target/manifest.json
head-manifest: target/manifest.json
Ships as two workflows, not one — and that split is the point. A pull_request
run from a fork gets a read-only token and no secrets, so it cannot post its own
results; switching to pull_request_target to fix that is the documented
"pwn request"
vulnerability. So the untrusted job analyses and holds nothing worth stealing, and a
separate trusted job posts the comment without ever checking out the contributor's code.
Results also go to the run's job summary, so a fork PR still sees them.
Setup and the security model →
Measured, not asserted
Across 7 public dbt projects (164 models), with no warehouse:
| 75.6% | of models resolve their output columns exactly | method → |
| 83.5% | have a usable schema | |
| 93.3% | on analytics-style projects, the target population | |
| 0 | false positives on the no-op corpus | 15 cases → |
| 11.5s | to analyse a 500-model project, cold |
The 93.3% rests on 2 projects and 15 models — directionally right and under-evidenced. The package
subset (73.8%, a harder population) is the conservative floor. Full method and threats to validity
are in ADR 000; the scripts in tools/ reproduce every
number here.
What it reports
| Rule | Change | Severity |
|---|---|---|
| WB001 | column removed | breaking · possibly · safe, by blast radius |
| WB002 | model removed | breaking if still referenced |
| WB003 | column added | safe |
| WB900 | model could not be analysed | info |
Renames, type changes and expression changes are inference rather than fact. They wait for later releases instead of shipping as guesses.
How it works
Two manifest.json files in, findings out — no warehouse, no network:
recover SQL → infer output columns → build column lineage → diff the two graphs
The last step is the one that matters: it is a graph diff, not a text diff. Reformatting and CTE
renames produce nothing, and a model whose columns changed because an upstream SELECT * changed
is caught even though its own file was never touched.
Honesty by design
- Never overclaim. Every finding carries a severity and an independent confidence. Coverage is always reported, so a clean result is never mistaken for a complete one.
- Absence of evidence is not evidence of absence. A removed column with no consumer is
safeonly when coverage is complete — otherwisepossibly_breaking. - Deterministic. No network calls, ever — enforced by a test that blocks sockets. No LLMs.
Limitations
Explicit by design, not apology. Full list in docs/limitations.md.
- Without
catalog.json,SELECT *over a source with no declared columns cannot be expanded. Reportedpartial, never guessed. (A star over a CTE resolves fine — most do.) - Macros needing a live warehouse (
run_query,adapter.get_relation) are unresolvable offline and say so by name. - Python models are out of scope, and are named as such rather than reported as a parse error.
- whatbreaks reasons about schema, not values. Whether your numbers changed is a different question, and Recce and Datafold answer it well.
Status
v0.1.0 — early, but real. Tested against real dbt projects and honest about what it could not
analyse. Not yet here: the GitHub Action, rename and type-change detection, suppressions. The JSON
output carries its own schema_version so tooling can pin while the shape settles.
Contributing
Found SQL whatbreaks gets wrong? A corpus case is the most useful possible bug report — one YAML file, no code. It reproduces the problem, documents the expectation, and becomes the regression test the moment it is fixed.
A break we failed to report is the more valuable half: false negatives mean someone merges a breaking change believing it is safe. See CONTRIBUTING.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whatbreaks-0.2.0.tar.gz.
File metadata
- Download URL: whatbreaks-0.2.0.tar.gz
- Upload date:
- Size: 153.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
554fe169d753f52af10e81c7ea5caf359d7c2c8e862136db7c8dc209a623fa8d
|
|
| MD5 |
72082c175d381608e65347d52b451295
|
|
| BLAKE2b-256 |
d39ab79a603528504b2de4bd00dfde97d350046b407c97090e5d2b9db88e9d95
|
Provenance
The following attestation bundles were made for whatbreaks-0.2.0.tar.gz:
Publisher:
release.yml on Ridadata/whatbreaks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whatbreaks-0.2.0.tar.gz -
Subject digest:
554fe169d753f52af10e81c7ea5caf359d7c2c8e862136db7c8dc209a623fa8d - Sigstore transparency entry: 2411323190
- Sigstore integration time:
-
Permalink:
Ridadata/whatbreaks@febb7d2f8bd4f871c8e365eca88496439300bf3a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Ridadata
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@febb7d2f8bd4f871c8e365eca88496439300bf3a -
Trigger Event:
push
-
Statement type:
File details
Details for the file whatbreaks-0.2.0-py3-none-any.whl.
File metadata
- Download URL: whatbreaks-0.2.0-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff021ed0cf68f238c63d5368a521b5b792d63cf0e708be39cd347951214b6bb5
|
|
| MD5 |
89ab99e27920eeb3018f1e17975e4cb4
|
|
| BLAKE2b-256 |
921134319c780d4e2513e26c9ebe76520c6e963795963af52eecae68a3eb3e86
|
Provenance
The following attestation bundles were made for whatbreaks-0.2.0-py3-none-any.whl:
Publisher:
release.yml on Ridadata/whatbreaks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whatbreaks-0.2.0-py3-none-any.whl -
Subject digest:
ff021ed0cf68f238c63d5368a521b5b792d63cf0e708be39cd347951214b6bb5 - Sigstore transparency entry: 2411323261
- Sigstore integration time:
-
Permalink:
Ridadata/whatbreaks@febb7d2f8bd4f871c8e365eca88496439300bf3a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Ridadata
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@febb7d2f8bd4f871c8e365eca88496439300bf3a -
Trigger Event:
push
-
Statement type: