mcpField
D
dbt-plan

dbt-plan

本地服务
presentjay.github.io开发工具21 天前更新

关于 dbt-plan

dbt-plan 静态分析编译后的 dbt SQL 变更,在执行前提示 DDL 风险。它只读取本地文件,不连接数据仓库,并可检查下游影响、契约和类型变更。

主要功能

  1. 01检测列的新增与删除
  2. 02依据模型配置评估 DDL 风险
  3. 03追踪删除列的下游影响
  4. 04检查强制契约和显式 CAST 变更
  5. 05输出文本、GitHub Markdown 和 JSON

适用场景

  1. 01在执行 dbt run 前审查架构变更
  2. 02在拉取请求中拦截破坏性变更
  3. 03向编码 Agent 提供结构化变更结果

MCP 相关

可选的 `dbt-plan-mcp` 服务向 MCP 客户端提供 `plan` 和 `snapshot` 工具。`plan` 结果包含判定、模型操作,以及单独列出的拒绝评估项。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"presentjay-dbt-plan": {
"command": "uvx",
"args": [
"dbt-plan"
]
}
}
}
启动命令uvx dbt-plan

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypidbt-plan0.18.4

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

dbt-plan

CI PyPI

静态分析工具,可在 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/DROPdbt 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安全性
tableanyCREATE OR REPLACE TABLESAFE
viewanyCREATE OR REPLACE VIEWSAFE
ephemeralany(无物理对象)SAFE
snapshotanyREVIEW REQUIREDWARNING
incrementalignore无架构 DDL;现有目标可能因列变更而失败变更或未知列时为 WARNING
incrementalfail构建失败WARNING
incrementalappend_new_columns仅 ADD COLUMNSAFE
incrementalsync_all_columnsADD + DROP COLUMN删除列时为 DESTRUCTIVE
any(模型已移除)MODEL REMOVEDDESTRUCTIVE
any(未知的 osc)UNKNOWN on_schema_changeWARNING
materialized_view / custom(未由你设置)UNKNOWN materializationWARNING
materialized_view / custom(由你设置)遵循 incremental 规则取决于 osc
any(contract: {enforced: true})CONTRACT VIOLATIONWARNING

在契约下,列声明的 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-commanddbt compile每个修订版本运行一次,共运行两次。
base-refPR 的基准分支要比较的修订版本。
project-dir.dbt 项目目录。
target-dirtarget相对于 project-dir 的 dbt 产物目录;如果 dbt 写入 build 等自定义路径,请设置此项。
dialect空保留 CLI 配置和 manifest 适配器检测结果,然后回退到 snowflake。仅在需要覆盖项目设置时设置。
versionlatest固定 dbt-plan 版本。
fail-ondestructive也可以是 warning 或 never。
summarytrue将报告写入作业步骤摘要。

输出包括 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 设置,请参阅 编译命令。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。