{
  "markdown": "# dbt-plan\n\nStatic analysis tool that warns about risky DDL changes before `dbt run`.\n\nLike `terraform plan` for dbt, and used the same way: you run it **before** the thing\nthat changes your warehouse, not only in CI afterwards.\n\nRuns on compiled SQL. It reads files and nothing else, so it works with any warehouse —\nSnowflake, BigQuery, Redshift, Postgres, DuckDB — through one code path.\n\n## What It Looks Like\n\n```\n$ dbt-plan check\n\ndbt-plan -- 2 model(s) changed\n\nDESTRUCTIVE  int_order_enriched (incremental, sync_all_columns)\n  DROP COLUMN  shipping_info\n  DROP COLUMN  billing_info\n  ADD COLUMN   shipping_city\n  Downstream: dim_customers, fct_orders (2 model(s))\n  >> BROKEN_REF  fct_orders: references dropped column(s): shipping_info\n\nSAFE  dim_customers (table)\n  CREATE OR REPLACE TABLE\n\ndbt-plan: 2 checked, 1 safe, 0 warning, 1 destructive, 1 cascade risk(s)\n```\n\n## What It Does\n\ndbt-plan analyzes compiled SQL diffs to catch dangerous schema changes at PR time:\n\n- **Column changes**: detects ADD/DROP COLUMN from SQL diff\n- **Risk assessment**: judges safety based on materialization x on_schema_change rules\n- **Cascade analysis**: finds downstream models that reference dropped columns\n- **Config changes**: detects materialization or on_schema_change policy changes\n- **Type changes**: compares explicit `CAST` types between revisions\n- **`SELECT *` resolution**: reads the columns from the CTEs of the same statement, and follows a `ref()` into the referenced model's compiled SQL\n\nIt does NOT execute anything, connect to any warehouse, or simulate `dbt run`. It reads files, compares them, and warns you.\n\n## Quick Start\n\n```bash\npip install dbt-plan\ndbt-plan run               # compile baseline → compile current → check\n```\n\n`dbt-plan run` does the whole thing in one command, and needs whatever credentials your\n`dbt compile` normally needs.\n\n### The loop it is built for\n\nOnce you have a baseline, the inner loop is a single sub-second command. Edit a model or a\nmacro, recompile, and see what `dbt run` would do — *before* running it:\n\n```bash\ndbt-plan snapshot          # once, on the revision you are changing from\n                           # ... edit models, edit macros ...\ndbt compile && dbt-plan check\n```\n\nMeasured on a project of 3 models, median of 3 runs:\n\n| step | time |\n|---|---|\n| `dbt compile` (Fusion) | 1.8 – 3.8 s |\n| **`dbt-plan check`** | **0.11 s** |\n| `dbt-plan snapshot` | 0.10 s |\n\n200 models, every one of them changed: **0.48 s**. The compile is the cost, and you were\ncompiling anyway — dbt-plan itself is fast enough to sit in the edit loop rather than at\nthe end of it.\n\n### Working with a coding agent\n\nAn agent editing models cannot eyeball a diff and hesitate. Give it the check and the\nreasons behind it:\n\n```bash\ndbt-plan agent-setup       # writes dbt-plan guidance into your AGENTS.md\ndbt-plan check --format json\n```\n\nThe guidance leads with what an agent most often gets wrong: adding a model to\n`ignore_models`, or downgrading `on_schema_change` from `sync_all_columns` to `ignore`,\nsilences a real finding without making the change safe.\n\nOr give it the check as an MCP tool:\n\n```bash\npip install 'dbt-plan[mcp]'\ndbt-plan-mcp                # stdio MCP server exposing `plan` and `snapshot`\n```\n\n`plan` returns the verdict, the per-model operations, and — separately — a `refusals`\nlist naming everything dbt-plan declined to judge. That separation is the point: a person\nreading \"safe\" may still glance at the diff, an agent reading it proceeds, so a\nnon-empty `refusals` must never be collapsed into the verdict.\n\nThe server is a separate package from the analysis core. The core is offline and\nsynchronous by design and `tests/test_invariants.py` fails the build on an `asyncio` or\nnetwork import anywhere inside it; an MCP server is both, so keeping them apart is what\nkeeps that guarantee provable.\n\nRegistry entry — the line below is how the MCP registry verifies that whoever publishes\nthe entry also owns this PyPI package, so it has to stay in the README that ships:\n\n```\nmcp-name: io.github.PresentJay/dbt-plan\n```\n\n### More commands\n\n```bash\ndbt-plan init              # Generate .dbt-plan.yml config + update .gitignore\ndbt-plan stats             # Analyze project readiness\ndbt-plan ci-setup          # Generate GitHub Actions workflow\ndbt-plan check --format github   # GitHub markdown output\ndbt-plan check --format json     # JSON for CI pipelines\ndbt-plan check --select model1   # Check specific model only\n```\n\n\n\n## Scope\n\ndbt-plan is a **static analysis warning tool**, not a runtime simulator.\n\n| In scope | Out of scope |\n|----------|-------------|\n| Column ADD/DROP detection from compiled SQL | `dbt run` simulation |\n| materialization × on_schema_change risk rules | Warehouse connection |\n| Cascade broken ref / build failure analysis | `seed` / `source` change detection |\n| Config change detection (materialization, osc) | `pre_hook` / `post_hook` DDL analysis |\n| Explicit `CAST` type changes | Type changes on uncast columns |\n| `SELECT *` resolved through CTEs and `ref()` | `SELECT *` over a source or a raw table |\n| CI exit codes + structured output | `full_refresh` mode judgment |\n\n**Design principle**: false warnings are OK, false safe is never OK.\n\n## When to use it\n\ndbt-plan answers a narrower question than the warehouse-connected tools (Recce,\nSQLMesh, data-diff) and costs nothing to run, so it works as the cheap gate in\nfront of them — and on the Fusion engine, which compiles without a warehouse\nconnection, that includes fork pull requests where they cannot run at all.\nSee [use cases](docs/use-cases.md) for the comparison, real timings, and what it\ngets wrong.\n\n## Deliberately Not Planned\n\nIdeas that look useful but contradict what this tool is:\n\n| Idea | Why not |\n|------|---------|\n| INFORMATION_SCHEMA query | Requires 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. |\n| Type changes on columns with no explicit `CAST` | The 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. |\n\n## DDL Prediction Rules\n\n| Materialization | on_schema_change | Predicted DDL | Safety |\n|-----------------|------------------|---------------|--------|\n| table | any | `CREATE OR REPLACE TABLE` | SAFE |\n| view | any | `CREATE OR REPLACE VIEW` | SAFE |\n| ephemeral | any | (no physical object) | SAFE |\n| snapshot | any | `REVIEW REQUIRED` | WARNING |\n| incremental | ignore | no DDL | SAFE |\n| incremental | fail | build failure | WARNING |\n| incremental | append_new_columns | `ADD COLUMN` only | SAFE |\n| incremental | sync_all_columns | `ADD + DROP COLUMN` | DESTRUCTIVE if columns removed |\n| any | (model removed) | `MODEL REMOVED` | DESTRUCTIVE |\n| any | (unknown osc) | `UNKNOWN on_schema_change` | WARNING |\n| materialized_view / custom | (none set) | `UNKNOWN materialization` | WARNING |\n| materialized_view / custom | (osc set) | follows the incremental rules | per osc |\n\n## CI Integration (GitHub Actions)\n\n```yaml\nname: dbt-plan\non:\n  pull_request:\n    paths: ['models/**', 'macros/**', 'dbt_project.yml']\n\njobs:\n  plan:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    env:\n      # Whatever your profiles.yml reads. `dbt compile` connects; dbt-plan does not.\n      SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}\n      SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}\n      SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0          # the base revision has to be in the clone\n          persist-credentials: false\n      - uses: actions/setup-python@v5\n        with: { python-version: '3.12' }\n      - run: pip install uv && uv sync\n\n      - uses: PresentJay/dbt-plan@v1\n```\n\nKeep the `pull_request` trigger. Never switch it to `pull_request_target` — `dbt compile`\nruns Jinja and macros written in the pull request, so that would hand your warehouse\ncredentials to code from any fork.\n\n| Input | Default | |\n|---|---|---|\n| `compile-command` | `dbt compile` | Runs twice, once per revision. |\n| `base-ref` | the PR base | The revision to compare against. |\n| `project-dir` | `.` | dbt project directory. |\n| `dialect` | `snowflake` | sqlglot dialect for parsing compiled SQL. |\n| `version` | latest | Pin a dbt-plan release. |\n| `fail-on` | `destructive` | Or `warning`, or `never`. |\n| `summary` | `true` | Write the report to the job step summary. |\n\nOutputs `verdict` (`safe` / `destructive` / `warning`), `exit-code`, and `report`\n(path to the JSON report), so a later step can comment on the PR or open a ticket.\n\nFor a workflow you own outright rather than a wrapped action, `dbt-plan ci-setup`\ngenerates one with the credential wiring and least-privilege notes inline. Details in\n[docs/ci-integration.md](docs/ci-integration.md).\n\n## How It Works\n\n```mermaid\nflowchart TD\n    A[dbt-plan snapshot] --> B[Save compiled SQL + manifest.json]\n\n    C[dbt-plan check] --> D[diff_compiled_dirs]\n    D --> E[base compiled SQL]\n    D --> F[current compiled SQL]\n    E --> G[extract_columns]\n    F --> H[extract_columns]\n    G --> I[base columns]\n    H --> J[current columns]\n    I --> K[column diff]\n    J --> K\n    K --> L[predict_ddl + manifest config]\n    L --> M{Safety?}\n    M -->|SAFE| N[exit 0]\n    M -->|WARNING| O[exit 2]\n    M -->|DESTRUCTIVE| P[exit 1 — block merge]\n    L --> Q[find_downstream]\n    Q --> R[format_text / format_github]\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, TDD workflow, and coding rules.\n\n### Architecture\n\n```\nsrc/dbt_plan/\n├── columns.py      # SQLGlot column extraction (multi-dialect)\n├── config.py       # .dbt-plan.yml + env var configuration\n├── predictor.py    # DDL risk assessment rules + cascade analysis\n├── manifest.py     # manifest.json parsing + downstream BFS\n├── diff.py         # compiled SQL directory comparison\n├── formatter.py    # text / GitHub markdown / JSON output\n└── cli.py          # CLI: snapshot, check, init, stats, run, ci-setup\n```\n\n### How to Contribute\n\n**Where to start:** the [open issues](https://github.com/PresentJay/dbt-plan/issues),\nparticularly those labelled [good first issue](https://github.com/PresentJay/dbt-plan/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).\nEach one says what it is, how it was found, and what has to be decided before code.\n\n**Design decisions:** See [docs/design-notes.md](docs/design-notes.md).\n\n## Supported\n\n- dbt-core 1.7+, and the dbt Fusion engine (verified against `2.0.0-preview.218`)\n- Any warehouse: Snowflake, BigQuery, Redshift, Postgres, DuckDB, etc. (`--dialect`)\n- Python 3.10+\n- CTE, UNION ALL, QUALIFY, window functions, VARIANT access\n\n## License\n\nApache-2.0\n",
  "bytes": 10918,
  "sha": "0fe422045de21a3f2fb4b4abf105bd494297fef81fbe9662b51360eb1a860140",
  "repo_slug": "presentjay/dbt-plan",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_presentjay_dbt_plan_32c12ece/readme"
}