dbt-plan

io.github.PresentJay由 GitHub 帳號 PresentJay 發布v0.18.6更新於 Nov 3, 2026

Predicts the DDL a dbt change will execute, before you run it. Reads files, not the warehouse.

概覽

AI 產生的概覽

dbt-plan 的本機 MCP 伺服器,讓助理在執行 dbt run 之前預測 dbt 變更會執行的 DDL,並標出有風險的 schema 變更。

可以做什麼
  • 對已編譯的 dbt 產物執行 plan 檢查,回傳 safe、warning 或 destructive 判定及逐模型操作
  • 為目前編譯 SQL 與 manifest 建立基準快照,供後續比較
  • 檢視預測的 DDL,例如新增或刪除欄位、替換資料表以及設定變更
  • 讀取獨立的 refusals 清單,了解分析拒絕判斷的內容
  • 將工具指向自訂專案目錄或產物目錄
適用情境
適合代理修改 dbt 模型或巨集時使用:在真正對倉儲執行之前,判斷該變更是否會刪除欄位、破壞下游模型或違反契約。它是倉儲連線型工具之前的低成本離線關卡,僅依賴編譯產物即可運作。
執行需求
以本機 stdio 程序執行,從 PyPI 安裝帶 mcp 附加項的 dbt-plan 套件;需要 Python 3.10+ 與 dbt-core 1.7+(或 dbt Fusion 引擎)。分析只讀取本機編譯 SQL 與 manifest 檔案,因此專案需先完成編譯;檢查本身不需要倉儲連線或憑證。
安裝前請注意
可選的 run 工作流程會呼叫你自己的 dbt compile 指令,可能需要倉儲憑證並執行專案中的巨集。分析是靜態的,不模擬 dbt run,因此 safe 判定並不保證變更無害;應把 refusals 清單視為結果的一部分,而不是併入判定。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 dbt-plan,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

dbt-plan

[CI] [PyPI]

Static analysis tool that warns about risky DDL changes before dbt run.

Like terraform plan for dbt, and used the same way: you run it before the thing that changes your warehouse, not only in CI afterwards.

Analyzes compiled SQL files from warehouses including Snowflake, BigQuery, Redshift, Postgres and DuckDB through one code path. SQL dialect support varies; see analysis limits.

What It Looks Like

$ 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_unresolvedExplanation 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)

What It Does

dbt-plan analyzes compiled SQL diffs to catch dangerous schema changes at PR time:

  • Column changes: detects ADD/DROP COLUMN from SQL diff
  • Risk assessment: judges safety based on materialization x on_schema_change rules
  • Cascade analysis: finds downstream models broken by a dropped column — the ones that name it, resolved against the project's own schema rather than matched as text; the ones that select * and lose it without their own file changing; and the tests whose fixtures pin it down. Names the exposures whose owners need telling
  • Contracts: reports a change an enforced contract will reject, in either direction
  • Config changes: detects materialization or on_schema_change policy changes
  • Type changes: compares explicit CAST types between revisions
  • SELECT * resolution: reads the columns from the CTEs of the same statement, and follows a ref() into the referenced model's compiled SQL

The analysis reads files, compares them, and warns; it does not connect to a warehouse or simulate dbt run. The optional dbt-plan run workflow invokes your compile command, which can require credentials and execute macros.

Quick Start

See a result without a warehouse

The repository includes before/after compiled SQL and a manifest, so you can try the analysis without installing dbt or configuring warehouse credentials:

bash
git clone --depth 1 https://github.com/PresentJay/dbt-plancd dbt-planpython -m pip install dbt-planbash examples/sample-project/run-example.sh

The sample prints text, GitHub markdown and JSON output. It intentionally contains a dropped column, so the inner check reports exit code 1 (destructive); the example script itself completes successfully. See the sample scenario for the expected finding.

Use it with your dbt project

bash
python -m pip install dbt-plandbt-plan run               # compile baseline → compile current → check

dbt-plan run does the whole thing in one command and needs whatever credentials your dbt compile normally needs. If your project already has compiled artifacts, use dbt-plan check directly; it only reads local files and does not connect to a warehouse.

The loop it is built for

Once you have a baseline, the inner loop is a single sub-second command. Edit a model or a macro, recompile, and see what dbt run would do — before running it:

bash
dbt-plan snapshot          # once, on the revision you are changing from                           # ... edit models, edit macros ...dbt compile && dbt-plan check

dbt-plan check reads local compiled artifacts. Its latency is separate from dbt compile, which can depend on your adapter, macros, and warehouse. See the reproducible CLI benchmark for measured workloads, raw samples, and commands to measure your machine.

Working with a coding agent

An agent editing models cannot eyeball a diff and hesitate. Give it the check and the reasons behind it:

bash
dbt-plan agent-setup       # writes dbt-plan guidance into your AGENTS.mddbt-plan check --format json

The guidance leads with what an agent most often gets wrong: adding a model to ignore_models, or downgrading on_schema_change from sync_all_columns to ignore, silences a real finding without making the change safe.

Or give it the check as an MCP tool:

bash
pip install 'dbt-plan[mcp]'dbt-plan-mcp                # stdio MCP server exposing `plan` and `snapshot`

plan returns the verdict, the per-model operations, and — separately — a refusals list naming everything dbt-plan declined to judge. That separation is the point: a person reading "safe" may still glance at the diff, an agent reading it proceeds, so a non-empty refusals must never be collapsed into the verdict.

Both tools accept target_dir when dbt writes artifacts outside the default target/, for example plan(project_dir=".", target_dir="build").

The server is a separate package from the analysis core. The core is offline and synchronous by design and tests/test_invariants.py fails the build on an asyncio or network import anywhere inside it; an MCP server is both, so keeping them apart is what keeps that guarantee provable.

Registry entry — the line below is how the MCP registry verifies that whoever publishes the entry also owns this PyPI package, so it has to stay in the README that ships:

mcp-name: io.github.PresentJay/dbt-plan

More commands

bash
dbt-plan init              # Generate .dbt-plan.yml config + update .gitignoredbt-plan stats             # Analyze project readinessdbt-plan ci-setup          # Generate GitHub Actions workflowdbt-plan check --format github   # GitHub markdown outputdbt-plan check --format json     # JSON for CI pipelinesdbt-plan run --against main           # compare with where this branch left maindbt-plan check --select fct_orders    # one modeldbt-plan check --select fct_orders+   # it and everything downstream

--select accepts model names and optional upstream/downstream + operators; comma-separated terms form a union. In the next minor release, unsupported or unknown selections fail with exit 3. Use explicit version names such as fct_orders_v2. See the selection contract and examples.

Scope

dbt-plan is a static analysis warning tool, not a runtime simulator.

In scopeOut of scope
Column ADD/DROP detection from compiled SQLdbt run simulation
materialization × on_schema_change risk rulesWarehouse connection
Cascade: broken refs, build failures, inherited column lossseed / source change detection
Config change detection (materialization, osc)pre_hook / post_hook DDL analysis
Unit test fixtures and exposure owners downstreamseed / source fixtures dbt-plan cannot read
Enforced-contract violations: names, and types by familyContract types compared more finely than family
Explicit CAST type changesType changes on uncast columns
SELECT * resolved through CTEs and ref()SELECT * over a source or a raw table
CI exit codes + structured outputfull_refresh mode judgment

Design principle: false warnings are OK, false safe is never OK.

When to use it

dbt-plan answers a narrower question than the warehouse-connected tools (Recce, SQLMesh, data-diff) and costs nothing to run, so it works as the cheap gate in front of them — and on the Fusion engine, which compiles without a warehouse connection, that includes fork pull requests where they cannot run at all. See use cases for the comparison, real timings, and what it gets wrong.

Deliberately Not Planned

Ideas that look useful but contradict what this tool is:

IdeaWhy not
INFORMATION_SCHEMA queryRequires a warehouse connection. dbt-plan reads files and nothing else, which is what lets it run wherever its input exists — including a fork's pull request, once the project compiles on Fusion.
Type changes on columns with no explicit CASTThe type is whatever the warehouse assigned, so seeing a change would mean asking it. Columns that are cast explicitly on both sides are compared — see below.

DDL Prediction Rules

Materializationon_schema_changePredicted DDLSafety
tableanyCREATE OR REPLACE TABLESAFE
viewanyCREATE OR REPLACE VIEWSAFE
ephemeralany(no physical object)SAFE
snapshotanyREVIEW REQUIREDWARNING
incrementalignoreno schema DDL; existing targets may fail on column changesWARNING on changed/unknown columns
incrementalfailbuild failureWARNING
incrementalappend_new_columnsADD COLUMN onlySAFE
incrementalsync_all_columnsADD + DROP COLUMNDESTRUCTIVE if columns removed
any(model removed)MODEL REMOVEDDESTRUCTIVE
any(unknown osc)UNKNOWN on_schema_changeWARNING
materialized_view / custom(not set by you)UNKNOWN materializationWARNING
materialized_view / custom(you set one)follows the incremental rulesper osc
any(contract: {enforced: true})CONTRACT VIOLATIONWARNING

Under a contract, a column's declared data_type is compared with its explicit CAST, by family -- text against number against date/time against boolean. varchar and text are the same family and not a finding; varchar and integer are a build failure. Comparing more finely means a per-adapter type table, and a wrong answer about a type is worse than no answer.

"Not set by you" means the author wrote no on_schema_change, in the model or in dbt_project.yml. dbt resolves one for every model regardless, so the resolved value asserts nothing; an explicit setting is a claim about how that materialization behaves and is honoured. dbt-plan reads unrendered_config to tell them apart.

An enforced contract inverts the rules above: dbt requires every column to be declared, so a column added to the SQL fails the build just as a removed one does. Names only — dbt compares its declared data_type against the warehouse, which dbt-plan does not read.

CI Integration (GitHub Actions)

yaml
name: dbt-planon:  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

This example assumes a checked-in uv.lock with your dbt adapter included. The virtual environment lives outside the checkout so changing revisions cannot replace it. With pip/requirements.txt, install your adapter before the Action and use the default dbt compile command. Run dbt deps first if your project uses dbt packages.

Keep the pull_request trigger. Never switch it to pull_request_target — dbt compile runs Jinja and macros written in the pull request, so that would hand your warehouse credentials to code from any fork.

InputDefault
compile-commanddbt compileRuns twice, once per revision.
base-refthe PR baseThe revision to compare against.
project-dir.dbt project directory.
target-dirtargetdbt artifact directory relative to project-dir; set this when dbt writes to a custom path such as build.
dialectemptyPreserve CLI configuration and manifest adapter detection, then fall back to snowflake. Set only to override the project.
versionlatestPin a dbt-plan release.
fail-ondestructiveOr warning, or never.
summarytrueWrite the report to the job step summary.

Outputs verdict (safe / destructive / warning), exit-code, and report (path to the JSON report), so a later step can comment on the PR or open a ticket.

For a workflow you own outright rather than a wrapped action, dbt-plan ci-setup generates one with credential wiring and least-privilege notes inline. The next-minor generator supports pyproject.toml/uv.lock and requirements.txt, installs dbt and dbt-plan in one environment, and separates summary rendering from its gate. The default FAIL_ON: destructive allows warnings but always blocks execution errors. Regenerate existing workflows to adopt it; see docs/ci-integration.md.

Exit codes

The next minor release separates execution failures from review findings (a breaking change from 0.15.x). This contract is implemented on main and is not yet released.

CodeMeaning
0No blocking findings under the configured policy.
1Destructive findings.
2Review required: SQL uncertainty or potential build/test failures.
3Execution failed: invalid input, missing artifacts, compile/recovery failure, or an internal error. No completed verdict.

warning_exit_code still controls review findings; its default remains 2. Setting it to 0 does not suppress execution errors. Code 3 is reserved and cannot be used for warnings. The Action fails on execution errors even with fail-on: never. See the migration guide before upgrading a CI script or pinned Action.

How It Works

mermaid
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]

Contributing

See CONTRIBUTING.md for development setup, TDD workflow, and coding rules.

First contribution? Choose a small, ready task: SQL fixtures, documentation or a sample-script fix, with exact expected results and no warehouse credentials required.

CI feedback for contributors: PRs to this repository get one automatically updated status comment with the tested commit, approval waits, and links to failed jobs. Comment /ci to refresh it, or /ci retry to retry a transient failure. External contributions still need a maintainer's execution approval; retrying does not bypass it. See commands and limits. This helper is for contributions to dbt-plan itself, separate from the GitHub Action you install in your own dbt project.

Architecture

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

How to Contribute

Where to start: the open issues, particularly those labelled good first issue. Each starter issue has an agreed scope, expected results and validation commands. Check its assignee and recent comments, then confirm ownership before starting.

Design decisions: See docs/design-notes.md.

Supported

  • dbt-core 1.7+, and the dbt Fusion engine (verified against 2.0.0-preview.218)
  • Snowflake, BigQuery, Redshift, Postgres, DuckDB and other mapped adapters: dialect inferred from the manifest. Unmapped adapters fall back to Snowflake; see analysis limits.
  • Python 3.10+
  • CTE, UNION ALL, QUALIFY, window functions, VARIANT access

License

Apache-2.0

For adapter dialect fallback, dbt Mesh consumers, and stars over sources or seeds, see analysis limits. For CLI compile_command argv semantics, the Action's shell command input, and dbt deps setup, see compile commands.

來源:README.md,提交 4caa03d

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

8
  1. v0.18.6最新Nov 3, 2026
  2. v0.18.5Oct 29, 2026
  3. v0.18.3Oct 26, 2026
  4. v0.18.2Oct 22, 2026
  5. v0.18.1Oct 10, 2026
  6. v0.18.0Oct 6, 2026
  7. v0.17.0Sep 25, 2026
  8. v0.16.1Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分