DEV Community

Cover image for zhao-cli: a free, deterministic breaking-change gate for dbt
allenhori
allenhori

Posted on

zhao-cli: a free, deterministic breaking-change gate for dbt

I kept hitting the same problem on almost every dbt project I worked on: a PR changes a column somewhere mid-DAG, and the only way to know what it actually breaks downstream is to either read the SQL by hand across every model that might reference it, or rebuild the whole project/downstream in CI and wait.

Neither scales past a DAG of any real size. So over the last few weeks, after hours, I built zhao-cli to fix that for myself , and since it turned out to actually work, I'm putting it out there.

This is a solo, weekend project. I've tested it against real dbt projects and, for the trickier calls (like whether adding a field to a STRUCT column should be treated as breaking), I went and verified it against a live Databricks workspace rather than guessing. But I haven't been able to cover every adapter/warehouse combination alone -- if you hit something that doesn't match your setup, an issue or a PR is genuinely welcome. I'd rather ship something honestly labeled
"early" than oversell it.

The problem

dbt's own state:modified comparison is syntactic -- any compiled-SQL text change counts as "modified," and everything downstream is assumed affected. Teams end up either rebuilding their whole downstream cone on every PR (slow CI), or leaning on a human reviewer to catch a removed column, a narrowed type, or a loosened join by reading SQL -- something nobody reliably does across a DAG of any real size.

zhao-cli: the breaking-change gate

zhao parses the compiled SQL itself and computes real column-level lineage between two states of your project, classifies each change against a fixed Rule catalog, and reports the exact models each change actually reaches -- never the whole DAG, never a guess.

$ zhao check --against main

Changed:
  model model.jaffle_shop.stg_customers:
    - column removed: last_name

Downstream impact:
  model model.jaffle_shop.dim_customers:
    [BREAKING] last_name removed from model model.jaffle_shop.stg_customers breaks reference via last_name (column-removed-with-active-references)

Summary: 1 model(s) changed, 1 column(s) changed, 1 breaking, 0 warning

Impacted models: dim_customers
Enter fullscreen mode Exit fullscreen mode

That's the whole review, in one CI step: exactly what changed, exactly what it breaks, and exactly which models to re-validate, not a guess at the whole downstream cone.

The analysis itself is entirely local: no LLM, no account, and it never reads or sends your actual data, nothing installed in your warehouse beyond what dbt run already needs. The one place a network call happens is resolving a git-native Baseline (dbt compile/dbt deps, the same as running dbt yourself) -- pass --state with an already-compiled manifest to skip that entirely, for a genuinely zero-network-call run.

It also exports an interactive, self-contained lineage graph, click a model or column to trace exactly what depends on it and what it depends on, search, filter, all in one offline HTML file:

zhao lineage graph -- clicking a model, expanding columns, tracing a calculated column's real upstream source

Live demo · Repo

How this compares

I'd rather be precise here than let anyone assume I haven't looked:

SQLMesh already does this well sqlmesh plan uses column-level lineage to classify changes as breaking or non-breaking, natively, for free. If you're already on SQLMesh, you have this.
zhao-cli exists for the much larger population of teams already on dbt who don't want to migrate platforms just to get it. It brings the same category of protection to a dbt project as it already stands.

Install

curl -fsSL https://raw.githubusercontent.com/allenhori/zhao-cli/master/scripts/install.sh | sh
# or: cargo install zhao-cli
Enter fullscreen mode Exit fullscreen mode

Licensing

zhao-cli is Apache-2.0 -- fully permissive, use it however you want, no strings.

Why "zhao(曌)"

Named for the character Empress Wu Zetian invented for herself: 明 (sun and moon) over 空 (sky), "illuminating everything below." Felt like an honest fit for a tool whose entire job is showing you exactly what a change touches, instead of leaving you to trace the DAG by hand.


Repo: zhao-cli

There's a second tool in the same family, zhao-dbt-plan, tackling a sharper dbt gap around microbatch backfills, more on that in a follow-up post in a few days.

If you try this and it breaks on your setup, please open an issue, that's exactly the kind of real-world coverage I can't get building this alone.

Top comments (0)