关于 dbt-plan
dbt-plan 静态分析编译后的 dbt SQL 变更,在执行前提示 DDL 风险。它只读取本地文件,不连接数据仓库,并可检查下游影响、契约和类型变更。
主要功能
- 01检测列的新增与删除
- 02依据模型配置评估 DDL 风险
- 03追踪删除列的下游影响
- 04检查强制契约和显式 CAST 变更
- 05输出文本、GitHub Markdown 和 JSON
适用场景
- 01在执行 dbt run 前审查架构变更
- 02在拉取请求中拦截破坏性变更
- 03向编码 Agent 提供结构化变更结果
MCP 相关
可选的 `dbt-plan-mcp` 服务向 MCP 客户端提供 `plan` 和 `snapshot` 工具。`plan` 结果包含判定、模型操作,以及单独列出的拒绝评估项。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
dbt-plan
静态分析工具,可在 dbt run 之前警告有风险的 DDL 变更。
它类似于 dbt 的 terraform plan,用法也一样:在运行会更改数据仓库的操作之前执行,而不只是事后在 CI 中运行。
通过一套代码路径分析 Snowflake、BigQuery、Redshift、Postgres 和 DuckDB 等数据仓库中的已编译 SQL 文件。SQL 方言支持情况各不相同;请参阅分析限制。
效果示例
$ dbt-plan check
dbt-plan -- 4 model(s) changed
dialect: snowflake (default; adapter: unknown)
baseline: unknown revision, unknown snapshot time
Review required: DESTRUCTIVE (before policy; exit policy is separate)
Next: Review the finding evidence and affected source files before running dbt.
Next: Resolve the unreadable downstream SQL and recompile before rerunning check.
DESTRUCTIVE int_order_enriched (incremental, sync_all_columns)
ADD COLUMN billing_method
ADD COLUMN shipping_city
DROP COLUMN billing_info
DROP COLUMN shipping_info
Downstream: dim_customers, fct_daily_sales (2 model(s))
>> BROKEN_REF fct_daily_sales: reads dropped column(s): shipping_info
SAFE dim_customers (table)
CREATE OR REPLACE TABLE
SAFE dim_publishers (table)
CREATE OR REPLACE TABLE
SAFE fct_daily_sales (incremental, append_new_columns)
ADD COLUMN total_sales
Causal explanations (before policy)
- DESTRUCTIVE cascade.broken_ref: source model.sample.int_order_enriched; affected model.sample.fct_daily_sales
reads dropped column(s): shipping_info
Source change: added billing_method, shipping_city; removed billing_info, shipping_info
Affected/evidence columns: (not recorded); evidence: legacy_cascade / unknown / provenance_unavailable; raw risk: broken_ref; waiver eligible: false
Review: provenance_unavailable
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
Step: model.sample.int_order_enriched -> model.sample.fct_daily_sales; exact direct compiled read; root attribution not established; removed shipping_info
Config: materialized=incremental; on_schema_change=None
- DESTRUCTIVE ddl.drop_column: source model.sample.int_order_enriched; affected model.sample.int_order_enriched
DROP COLUMN; column: billing_info
Source change: added billing_method, shipping_city; removed billing_info, shipping_info
Affected/evidence columns: billing_info, billing_method, customer_id, order_date, order_id, revenue, shipping_city, shipping_info; 1 columns omitted; evidence: compiled_sql / exact / column_diff_checked; raw risk: destructive; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/int_order_enriched.sql
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
- DESTRUCTIVE ddl.add_column: source model.sample.int_order_enriched; affected model.sample.int_order_enriched
ADD COLUMN; column: billing_method
Source change: added billing_method, shipping_city; removed billing_info, shipping_info
Affected/evidence columns: billing_info, billing_method, customer_id, order_date, order_id, revenue, shipping_city, shipping_info; 1 columns omitted; evidence: compiled_sql / exact / column_diff_checked; raw risk: destructive; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/int_order_enriched.sql
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
- DESTRUCTIVE ddl.add_column: source model.sample.int_order_enriched; affected model.sample.int_order_enriched
ADD COLUMN; column: shipping_city
Source change: added billing_method, shipping_city; removed billing_info, shipping_info
Affected/evidence columns: billing_info, billing_method, customer_id, order_date, order_id, revenue, shipping_city, shipping_info; 1 columns omitted; evidence: compiled_sql / exact / column_diff_checked; raw risk: destructive; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/int_order_enriched.sql
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
- DESTRUCTIVE ddl.drop_column: source model.sample.int_order_enriched; affected model.sample.int_order_enriched
DROP COLUMN; column: shipping_info
Source change: added billing_method, shipping_city; removed billing_info, shipping_info
Affected/evidence columns: billing_info, billing_method, customer_id, order_date, order_id, revenue, shipping_city, shipping_info; 1 columns omitted; evidence: compiled_sql / exact / column_diff_checked; raw risk: destructive; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/int_order_enriched.sql
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
- WARNING input.refusal: source model.sample.dim_customers; affected model.sample.dim_customers
Unresolved compiled SQL read from int_order_enriched while checking int_order_enriched; affected columns: billing_info, shipping_info
Source change: not recorded
Affected/evidence columns: (not recorded); evidence: input / unknown / read_unresolved; raw risk: warning; waiver eligible: false
Review: read_unresolved
Source config: materialized=table; on_schema_change=None
Affected config: materialized=table; on_schema_change=None
- SAFE ddl.replace_table: source model.sample.dim_customers; affected model.sample.dim_customers
CREATE OR REPLACE TABLE
Source change: not recorded
Affected/evidence columns: country, created_at, customer_id, customer_tier; evidence: compiled_sql / exact / column_diff_checked; raw risk: safe; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/dim_customers.sql
Source config: materialized=table; on_schema_change=None
Affected config: materialized=table; on_schema_change=None
- SAFE ddl.replace_table: source model.sample.dim_publishers; affected model.sample.dim_publishers
CREATE OR REPLACE TABLE
Source change: not recorded
Affected/evidence columns: end_date, publisher_id, publisher_name, start_date; evidence: compiled_sql / exact / column_diff_checked; raw risk: safe; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/dim_publishers.sql
Source config: materialized=table; on_schema_change=None
Affected config: materialized=table; on_schema_change=None
- SAFE ddl.add_column: source model.sample.fct_daily_sales; affected model.sample.fct_daily_sales
ADD COLUMN; column: total_sales
Source change: added total_sales; removed (not recorded)
Affected/evidence columns: order_count, order_date, shipping_info, store_id, total_sales, unique_customers; evidence: compiled_sql / exact / column_diff_checked; raw risk: safe; waiver eligible: true
Compiled SQL (not a source/Jinja line): target/compiled/sample/models/fct_daily_sales.sql
Source config: materialized=incremental; on_schema_change=None
Affected config: materialized=incremental; on_schema_change=None
Observation: model.sample.int_order_enriched -> model.sample.dim_customers; unknown read check; not a column-flow edge; columns (not recorded); read_unresolved
Explanation detail omitted: 0 associations; 0 graph edges not displayed. Full relevant graph and findings: JSON.
External consumer coverage: unknown. Exposures are declared consumers only.
dbt-plan: 4 checked, 3 safe, 0 warning, 1 destructive, 1 cascade risk(s); overall review: DESTRUCTIVE (before policy)
功能
dbt-plan 会分析已编译 SQL 的差异,在 PR 阶段发现危险的架构变更:
- 列变更:从 SQL 差异中检测 ADD/DROP COLUMN
- 风险评估:根据 materialization 和 on_schema_change 规则判断安全性
- 级联分析:查找因删除列而出问题的下游模型——包括那些显式引用该列、根据项目自身架构解析而非按文本匹配的模型;那些使用
*选择列、即使自身文件未变也会丢失该列的模型;以及固定了该列的测试数据。还会列出需要通知负责人的 exposures - 契约:报告强制执行的契约会拒绝的变更,包括任一方向的变更
- 配置变更:检测 materialization 或 on_schema_change 策略变更
- 类型变更:比较不同版本之间显式
CAST的类型 SELECT *解析:读取同一语句中 CTE 的列,并跟踪ref()到被引用模型的已编译 SQL
分析会读取并比较文件,然后发出警告;它不会连接数据仓库,也不会模拟 dbt run。可选的 dbt-plan run 工作流会调用你的编译命令,这可能需要凭据并执行宏。
快速开始
不连接数据仓库查看结果
仓库中包含变更前后的已编译 SQL 和 manifest,因此你无需安装 dbt 或配置数据仓库凭据,就可以试用分析功能:
git clone --depth 1 https://github.com/PresentJay/dbt-plan
cd dbt-plan
python -m pip install dbt-plan
bash examples/sample-project/run-example.sh
示例会输出文本、GitHub Markdown 和 JSON。它特意包含了一个被删除的列,因此内部检查会返回退出代码 1(destructive);示例脚本本身则会成功完成。有关预期发现,请参阅示例场景。
在 dbt 项目中使用
python -m pip install dbt-plan
dbt-plan run # compile baseline → compile current → check
dbt-plan run 会通过一条命令完成整个流程,并且需要 dbt compile 通常所需的凭据。如果项目已经有已编译的产物,可以直接使用 dbt-plan check;它只会读取本地文件,不会连接数据仓库。
为其设计的循环
有了基线之后,内部循环只需运行一条亚秒级命令。编辑模型或宏,重新编译,就能在运行 dbt run 之前查看它会执行什么:
dbt-plan snapshot # once, on the revision you are changing from
# ... edit models, edit macros ...
dbt compile && dbt-plan check
dbt-plan check 会读取本地已编译产物。它的延迟与 dbt compile 分开计算;后者可能取决于你的适配器、宏和数据仓库。
有关已测工作负载、原始样本以及如何测量你的机器性能,请参阅可复现的 CLI 基准测试。
与编码 Agent 协作
Agent 修改模型时,不能只看着差异犹豫不决。向它提供检查结果及其背后的原因:
dbt-plan agent-setup # writes dbt-plan guidance into your AGENTS.md
dbt-plan check --format json
这份指南开门见山地指出 Agent 最常犯的错误:把模型添加到 ignore_models,或将 on_schema_change 从 sync_all_columns 降级为 ignore,只会掩盖真实问题,并不会让更改变得安全。
也可以将检查作为 MCP 工具提供给它:
pip install 'dbt-plan[mcp]'
dbt-plan-mcp # stdio MCP server exposing `plan` and `snapshot`
plan 会返回判定结果、每个模型的操作,以及单独列出的 refusals 清单,其中列明了 dbt-plan 拒绝判定的所有事项。这种区分正是关键:阅读“安全”的人可能仍会看一眼差异,而 Agent 看到后就会继续执行,因此绝不能将非空的 refusals 合并到判定结果中。
当 dbt 将产物写到默认 target/ 目录之外时,这两个工具都接受 target_dir,例如 plan(project_dir=".", target_dir="build")。
MCP 服务是独立于分析核心的单独软件包。核心按设计离线运行且为同步操作,tests/test_invariants.py 会在核心任意位置导入 asyncio 或网络模块时让构建失败;MCP 服务则两者兼有,因此将它们分开才能确保这一保证可验证。
注册表条目——以下这一行是 MCP 注册表验证发布该条目的人同时拥有此 PyPI 软件包的方式,因此必须保留在随软件包发布的 README 中:
mcp-name: io.github.PresentJay/dbt-plan
更多命令
dbt-plan init # Generate .dbt-plan.yml config + update .gitignore
dbt-plan stats # Analyze project readiness
dbt-plan ci-setup # Generate GitHub Actions workflow
dbt-plan check --format github # GitHub markdown output
dbt-plan check --format json # JSON for CI pipelines
dbt-plan run --against main # compare with where this branch left main
dbt-plan check --select fct_orders # one model
dbt-plan check --select fct_orders+ # it and everything downstream
--select 接受模型名称和可选的上游/下游 + 运算符;以逗号分隔的条件会组成并集。在下一个次要版本中,不受支持或未知的选择将以退出码 3 失败。请使用显式版本名称,例如 fct_orders_v2。请参阅选择规则和示例。
范围
dbt-plan 是一种静态分析警告工具,而非运行时模拟器。
| 范围内 | 范围外 |
|---|---|
| 根据编译后的 SQL 检测列 ADD/DROP | dbt run 模拟 |
| materialization × on_schema_change 风险规则 | 数据仓库连接 |
| 级联:引用失效、构建失败、继承列丢失 | seed/source 变更检测 |
| 配置变更检测(materialization、osc) | pre_hook/post_hook DDL 分析 |
| 单元测试夹具及下游 exposure 所有者 | dbt-plan 无法读取的 seed/source 夹具 |
| 强制契约违规:名称及按类型族划分的类型 | 契约类型的比较精细度超出类型族 |
显式 CAST 类型变更 | 未经强制转换的列的类型变更 |
通过 CTE 和 ref() 解析 SELECT * | 对 source 或原始表使用的 SELECT * |
| CI 退出码 + 结构化输出 | full_refresh 模式判定 |
设计原则:错误警告可以接受,错误地判定为安全则绝不接受。
适用场景
与连接数据仓库的工具(Recce、SQLMesh、data-diff)相比,dbt-plan 回答的问题范围更窄,而且运行成本为零,因此适合作为它们前面的低成本关卡;对于无需连接数据仓库即可编译的 Fusion 引擎,它也适用于无法运行这些工具的分支拉取请求。请参阅使用场景,了解比较、实际运行时间及其不足之处。
明确不计划实现
看似有用、但与此工具定位相悖的想法:
| 想法 | 原因 |
|---|---|
| 查询 INFORMATION_SCHEMA | 需要连接数据仓库。dbt-plan 只读取文件,不做其他操作,因此只要输入文件存在,就能在任何地方运行——包括项目在 Fusion 上完成编译后的分支拉取请求。 |
未显式 CAST 的列的类型变更 | 类型由数据仓库决定,因此要发现变更就必须查询数据仓库。两侧都显式转换的列会进行比较——见下文。 |
DDL 预测规则
| 实体化方式 | on_schema_change | 预测的 DDL | 安全性 |
|---|---|---|---|
| table | any | CREATE OR REPLACE TABLE | SAFE |
| view | any | CREATE OR REPLACE VIEW | SAFE |
| ephemeral | any | (无物理对象) | SAFE |
| snapshot | any | REVIEW REQUIRED | WARNING |
| incremental | ignore | 无架构 DDL;现有目标可能因列变更而失败 | 变更或未知列时为 WARNING |
| incremental | fail | 构建失败 | WARNING |
| incremental | append_new_columns | 仅 ADD COLUMN | SAFE |
| incremental | sync_all_columns | ADD + DROP COLUMN | 删除列时为 DESTRUCTIVE |
| any | (模型已移除) | MODEL REMOVED | DESTRUCTIVE |
| any | (未知的 osc) | UNKNOWN on_schema_change | WARNING |
| materialized_view / custom | (未由你设置) | UNKNOWN materialization | WARNING |
| materialized_view / custom | (由你设置) | 遵循 incremental 规则 | 取决于 osc |
| any | (contract: {enforced: true}) | CONTRACT VIOLATION | WARNING |
在契约下,列声明的 data_type 会按类型族与其显式 CAST 进行比较:文本、数字、日期/时间和布尔值。varchar 和 text 属于同一类型族,不会被判定为问题;varchar 和 integer 则会导致构建失败。若要进行更精细的比较,就需要为每种适配器维护类型表;对类型给出错误判断,比不作判断更糟。
“未由你设置”是指作者未在模型或 dbt_project.yml 中写入 on_schema_change。无论如何,dbt 都会为每个模型解析出一个值,因此解析出的值不能说明任何问题;显式设置则表示你对该实体化方式行为的声明,并会得到遵循。dbt-plan 会读取 unrendered_config 来区分两者。
强制执行的契约会反转上述规则:dbt 要求声明每一列,因此 SQL 中新增列和移除列一样,都会导致构建失败。这里只比较名称:dbt 会将声明的 data_type 与数据仓库中的类型比较,而 dbt-plan 不会读取数据仓库。
CI 集成(GitHub Actions)
name: dbt-plan
on:
pull_request:
jobs:
plan:
runs-on: ubuntu-latest
permissions:
contents: read
env:
# Whatever your profiles.yml reads. `dbt compile` connects; dbt-plan does not.
UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/project-venv
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the base revision has to be in the clone
persist-credentials: false
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install uv
- run: uv sync --locked
- uses: PresentJay/dbt-plan@v1
with:
compile-command: uv run --no-sync dbt compile
此示例假设已将包含 dbt 适配器的 uv.lock 提交到仓库。虚拟环境位于检出目录之外,这样切换修订版本就不会替换它。若使用 pip/requirements.txt,请在运行 Action 前安装适配器,并使用默认的 dbt compile 命令。如果项目使用 dbt 包,请先运行 dbt deps。
保留 pull_request 触发器。切勿将其改为 pull_request_target——dbt compile 会运行 pull request 中编写的 Jinja 和宏,这样会将数据仓库凭据交给任何 fork 中的代码。
| 输入项 | 默认值 | |
|---|---|---|
compile-command | dbt compile | 每个修订版本运行一次,共运行两次。 |
base-ref | PR 的基准分支 | 要比较的修订版本。 |
project-dir | . | dbt 项目目录。 |
target-dir | target | 相对于 project-dir 的 dbt 产物目录;如果 dbt 写入 build 等自定义路径,请设置此项。 |
dialect | 空 | 保留 CLI 配置和 manifest 适配器检测结果,然后回退到 snowflake。仅在需要覆盖项目设置时设置。 |
version | latest | 固定 dbt-plan 版本。 |
fail-on | destructive | 也可以是 warning 或 never。 |
summary | true | 将报告写入作业步骤摘要。 |
输出包括 verdict(safe / destructive / warning)、exit-code 和 report
(JSON 报告的路径),方便后续步骤在 PR 中发表评论或创建工单。
如果你希望完全自主控制工作流,而不是使用封装好的 Action,dbt-plan ci-setup
可以生成一个工作流,其中内置了凭据配置和最小权限说明。下一次次要版本中的生成器支持
pyproject.toml/uv.lock 和 requirements.txt,会在同一个环境中安装 dbt 和 dbt-plan,
并将摘要渲染与门禁判断分开。默认的
FAIL_ON: destructive 允许警告通过,但始终会阻止执行错误。重新生成现有工作流即可采用此设置;请参阅 docs/ci-integration.md。
退出代码
下一个次要版本将执行失败与审查发现分开(这是相对于 0.15.x 的破坏性变更)。此约定已在 main 分支实现,尚未发布。
| 代码 | 含义 |
|---|---|
0 | 根据已配置的策略,没有阻止性发现。 |
1 | 破坏性发现。 |
2 | 需要审查:SQL 存在不确定性,或可能导致构建/测试失败。 |
3 | 执行失败:输入无效、缺少构件、编译/恢复失败或内部错误。未完成判定。 |
warning_exit_code 仍控制审查发现;其默认值仍为 2。
将其设为 0 不会屏蔽执行错误。代码 3 为保留代码,不能用于警告。
即使设置了 fail-on: never,执行错误仍会导致 Action 失败。升级 CI 脚本或固定版本的 Action 前,请参阅迁移指南。
工作原理
flowchart TD
A[dbt-plan snapshot] --> B[Save compiled SQL + manifest.json]
C[dbt-plan check] --> D[diff_compiled_dirs]
D --> E[base compiled SQL]
D --> F[current compiled SQL]
E --> G[extract_columns]
F --> H[extract_columns]
G --> I[base columns]
H --> J[current columns]
I --> K[column diff]
J --> K
K --> L[predict_ddl + manifest config]
L --> M{Safety?}
M -->|SAFE| N[exit 0]
M -->|WARNING| O[exit 2]
M -->|DESTRUCTIVE| P[exit 1 — block merge]
L --> Q[find_downstream]
Q --> R[format_text / format_github]
贡献
有关开发环境设置、TDD 工作流和编码规则,请参阅 CONTRIBUTING.md。
首次贡献? 选择一项范围较小且已准备就绪的任务: SQL 测试数据、文档或示例脚本修复,均提供明确的预期结果, 且无需仓库凭据。
面向贡献者的 CI 反馈: 对此仓库的 PR 会收到一条自动更新的状态评论,其中包含经过测试的提交、审批等待情况,以及失败作业的链接。评论 /ci 可刷新状态,评论 /ci retry 可重试临时故障。
外部贡献仍需维护者批准执行;重试不会绕过此要求。请参阅命令和限制。
此辅助工具用于向 dbt-plan 本身贡献,与您在自己的 dbt 项目中安装的 GitHub Action 相互独立。
架构
src/dbt_plan/
├── columns.py # SQLGlot column extraction (multi-dialect)
├── config.py # .dbt-plan.yml + env var configuration
├── predictor.py # DDL risk assessment rules + cascade analysis
├── manifest.py # manifest.json parsing + downstream BFS
├── diff.py # compiled SQL directory comparison
├── formatter.py # text / GitHub markdown / JSON output
└── cli.py # CLI: snapshot, check, init, stats, run, ci-setup
如何贡献
从哪里开始: 查看未解决的问题, 尤其是标记为 good first issue 的问题。 每项入门问题都已商定范围、预期结果和验证命令。 开始前,请查看负责人和近期评论,并确认任务归属。
设计决策: 请参阅 docs/design-notes.md。
支持情况
- dbt-core 1.7+,以及 dbt Fusion 引擎(已通过
2.0.0-preview.218验证) - Snowflake、BigQuery、Redshift、Postgres、DuckDB 及其他已映射的适配器:方言根据清单推断。未映射的适配器会回退到 Snowflake;请参阅分析限制。
- Python 3.10+
- CTE、UNION ALL、QUALIFY、窗口函数、VARIANT 访问
许可证
Apache-2.0
有关适配器方言回退、dbt Mesh 消费者,以及对来源或种子的星标,请参阅分析限制。有关 CLI compile_command argv 语义、Action 的 shell 命令输入,以及 dbt deps 设置,请参阅
编译命令。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。