<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: allenhori</title>
    <description>The latest articles on DEV Community by allenhori (@allenhori).</description>
    <link>https://dev.to/allenhori</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4068288%2F60741c35-73d4-4285-8213-5d45458f8f95.jpg</url>
      <title>DEV Community: allenhori</title>
      <link>https://dev.to/allenhori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/allenhori"/>
    <language>en</language>
    <item>
      <title>zhao-cli: a free, deterministic breaking-change gate for dbt</title>
      <dc:creator>allenhori</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:42:29 +0000</pubDate>
      <link>https://dev.to/allenhori/zhao-cli-a-free-deterministic-breaking-change-gate-for-dbt-aa6</link>
      <guid>https://dev.to/allenhori/zhao-cli-a-free-deterministic-breaking-change-gate-for-dbt-aa6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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 &lt;code&gt;STRUCT&lt;/code&gt; 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&lt;br&gt;
"early" than oversell it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;dbt's own &lt;code&gt;state:modified&lt;/code&gt; 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.&lt;/p&gt;
&lt;h2&gt;
  
  
  zhao-cli: the breaking-change gate
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;zhao&lt;/code&gt; parses the compiled SQL itself and computes &lt;em&gt;real&lt;/em&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;zhao check &lt;span class="nt"&gt;--against&lt;/span&gt; main

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

Downstream impact:
  model model.jaffle_shop.dim_customers:
    &lt;span class="o"&gt;[&lt;/span&gt;BREAKING] last_name removed from model model.jaffle_shop.stg_customers breaks reference via last_name &lt;span class="o"&gt;(&lt;/span&gt;column-removed-with-active-references&lt;span class="o"&gt;)&lt;/span&gt;

Summary: 1 model&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; changed, 1 column&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; changed, 1 breaking, 0 warning

Impacted models: dim_customers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;dbt run&lt;/code&gt; already needs. The one place a network call happens is resolving a git-native Baseline (&lt;code&gt;dbt compile&lt;/code&gt;/&lt;code&gt;dbt deps&lt;/code&gt;, the same as running &lt;code&gt;dbt&lt;/code&gt; yourself) -- pass &lt;code&gt;--state&lt;/code&gt; with an already-compiled manifest to skip that entirely, for a genuinely zero-network-call run.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fziw4s2u18cvwwl11ks0m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fziw4s2u18cvwwl11ks0m.gif" alt="zhao lineage graph -- clicking a model, expanding columns, tracing a calculated column's real upstream source" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://htmlpreview.github.io/?https://github.com/allenhori/zhao-cli/blob/master/docs/assets/lineage-demo.html" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt; · &lt;a href="https://github.com/allenhori/zhao-cli" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares
&lt;/h2&gt;

&lt;p&gt;I'd rather be precise here than let anyone assume I haven't looked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLMesh&lt;/strong&gt; already does this well &lt;code&gt;sqlmesh plan&lt;/code&gt; uses column-level lineage to classify changes as breaking or non-breaking, natively, for free. If you're already on SQLMesh, you have this.&lt;br&gt;
&lt;code&gt;zhao-cli&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/allenhori/zhao-cli/master/scripts/install.sh | sh
&lt;span class="c"&gt;# or: cargo install zhao-cli&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Licensing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;zhao-cli&lt;/code&gt; is Apache-2.0 -- fully permissive, use it however you want, no strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "zhao(曌)"
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/allenhori/zhao-cli" rel="noopener noreferrer"&gt;zhao-cli&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>rust</category>
      <category>dataengineering</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
