Change one model. Rebuild only what it reaches.

Freshet reads your SQL models and answers one question: what must rebuild? It follows lineage to every model a change reaches, and narrows incremental ones to the partitions that moved. On 10,240 models it answers in about a second. It never runs a query. Your orchestrator does that.

Two changes land: late rows restated into events for 2026-06-20, and accounts reformatted. Models shown in topological order.

  1. accounts

    table

    Reformat only

    Whitespace and casing changed; the syntax tree didn't. The fingerprint holds, so nothing below it rebuilds.

    0 CU

  2. events

    incremental · day on dt

    1 partition

    Restated for 2026-06-20: insert_overwrite over that one partition.

    0.100 CU

  3. plans

    table

    Unchanged

    Nothing it reads changed.

    0 CU

  4. fx_rates

    view

    Unchanged

    Nothing it reads changed.

    0 CU

  5. daily_rollup

    incremental · merge on dt

    1 partition

    Downstream of events via [dt], so the same single day.

    0.100 CU

  6. account_mrr

    table

    Unchanged

    Reads accounts, whose fingerprint held. A file checksum would have rebuilt it.

    0 CU

  7. invoices

    table

    Unchanged

    Reads accounts and fx_rates. Neither changed.

    0 CU

  8. user_dim

    table

    Full refresh

    Downstream of events via [last_seen, user_id]. A table has no partitions to replace, so the plan widens it and says why.

    1.000 CU

How Freshet decides

The shelf above is a real plan shape. This is the command that prints it, and the three rules behind every line.

Cosmetic edits never rebuild
Freshet parses each model and hashes its syntax tree, not its text. Reformat, re-case or re-comment a model and its fingerprint doesn't move.
Lineage down to the column
Every rebuild names the columns that carry it: user_dim is downstream of events through [last_seen, user_id]. Pointed at a dbt project, freshet explain diffs column by column and skips the models that read none of the changed columns. In 0.1.0, freshet plan treats a code change as touching every column of the model it changes.
Partitions first. Widen, never narrow
Incremental models replace only the partitions that moved, using lookback windows, grains and watermarks. When a replacement can't be honoured, the plan widens it to a full refresh and prints the reason. A narrowed write would leave stale rows, and nothing downstream would catch it.
freshet restate
$ freshet restate --project ./models --model events --partitions 2026-06-20
backfill plan (3 model(s), topological order):
  events          partitions[2026-06-20]   (directly changed)
                  └─ writes: insert_overwrite over 1 partition(s) on `dt`
  daily_rollup    partitions[2026-06-20]   (downstream of events via [dt])
                  └─ writes: merge over 1 partition(s) on key [dt]
  user_dim        full                     (downstream of events via [last_seen,user_id])
                  └─ writes: full_refresh over the whole relation

1 model(s) widened to a full refresh of the whole relation:
  user_dim        the model is a table, so it has no partitions to replace in place
Output from the README, columns trimmed to fit. Add --sql postgres, snowflake, bigquery, databricks or redshift and the plan also carries the statements for your runner to execute.

The firing record

We exported one generated project of 10,240 models as dbt, SQLMesh and Freshet. Each tool ran its own command as a whole process: one warm-up, five timed runs, median shown. We compared times only where the answers matched. For the one-model edit, every tool picked the same 80 models.

What must rebuild after m_0_0 gains a WHERE? Every tool answers 80.

  1. Freshet 0.1.0 1.039 s plan
  2. dbt v2 2.0.5 12.865 s 12× slower
  3. dbt-core 1.12.5 80.322 s 77× slower
  4. SQLMesh 0.236.2 486.657 s 468× slower

Linear scale: the longest run in each question fills the row.

Run A: 10,240 models (80 layers × 128, 8 columns), median of five, seconds. One 4-core containerised Xeon at 2.80 GHz, commit 65ce159, file results/compare-2026-09-22-all-tools.json.
QuestionFreshet 0.1.0dbt-core 1.12.5dbt v2 2.0.5 betaSQLMesh 0.236.2
Parse every model, build the DAG0.81073.57710.201101.317
Same, cache warm0.333*12.4301.21375.460
Nothing changed (all answer 0)1.11377.80513.023181.049
One model edited (all answer 80)1.03980.32212.865486.657
Nothing built yet (all answer N)1.354no such stepno such step521.904

* A curl to a running freshet-serve that parsed the project once at start-up. A long-running daemon is a different architecture from the others' per-run caches. Freshet's CLI keeps no parse cache, so its other rows are cold.

Same answer, different work. Freshet's plan also derives column lineage, fingerprints every model's syntax tree and prices the plan. dbt ls compares file checksums and selects descendants. SQLMesh's plan --explain renders every query and computes intervals. What they share is the answer you wait for.

Current code, re-timed. Run A predates Freshet's lineage fixes. Run B re-timed Freshet at 7e68f20 with dbt v2 as a control: plan took 0.823 s against dbt v2's 10.535 s, and the warm daemon replan 0.295 s. Run B's machine was faster, and the control moved by as much as Freshet did, so we don't mix runs. In-process, a warm replan on 10,240 models is 30–37 ms on one core.

State the whole team can commit to

A state.json in git is last-writer-wins. When two CI jobs plan at once, the second commit silently overwrites the first one's record of what was rebuilt. The next plan then under-rebuilds, and nobody notices.

Point --state at hosted state instead. Every commit is a compare-and-swap against the version the plan was computed from. If another commit landed in between, nothing is written: Freshet exits 3 and tells you to re-plan.

submit never commits state. It hands the plan to your runner, and you commit only after the run has succeeded. A --budget caps a plan at N compute units, reports the headroom left, and fails the pull request whose migration would rewrite the warehouse.

two CI jobs, one state
$ export FRESHET_STATE_TOKEN=…
$ freshet plan --project ./models --state https://…/state/v1 --json > plan.json
hosted state: version 7 at https://…/state/v1

# … run plan.json; once it has succeeded:
$ freshet plan --project ./models --state https://…/state/v1 \
    --commit --expect-version 7
hosted state at https://…/state/v1 moved: this plan was computed
against version 7, and the state is now at version 8 — another
commit landed after this plan was computed; re-plan. Nothing was
committed.
$ echo $?
3

Not a dbt feature. A planner you can embed.

dbt's state selection and its Rust engine both live inside dbt. Freshet stands alone. Any orchestrator reads its plans through a frozen JSON contract, and every surface below calls the same Rust core.

CLIfreshet plan · restate · fingerprint · graph · submitBuild it with --no-default-features for a binary with no HTTP or TLS stack.
Daemonfreshet-serveKeeps the parsed project warm over HTTP. A replan on 10,240 models takes 30–37 ms.
Pythonfreshet-planner · freshet-sdkThe planner in-process as an abi3 wheel, or a standard-library-only client for the daemon.
Airflowfreshet_airflow.build_dagTurns a plan into a DAG with one task per model, wired from the plan's own edges.
dagronfreshet submitPosts the plan and its CU estimate to a dagron run graph, self-hosted or on Cloud.
C ABIplanner_planA cdylib for any language with an FFI. Every entry point catches panics.
Browsereffort mapThe planner compiled to WebAssembly, about 359 KiB gzipped. It answers "what will this PR rebuild?" without a server.

Plan for free. Pay for the state you share.

Planning costs milliseconds of one core, so no edition meters plans. Replan on every pull request. The planner is the same everywhere. What changes is who runs the state it commits to.

  1. dagron Cloud

    Hosted state, operated for you.

    Operated

    • Hosted state at /state/v1, with compare-and-swap commits for every CI job and engineer
    • Backed up, migrated and on call: we run the Postgres behind it
    • Run rebuilds on your own compute, or on dagron's workers metered in compute units. Column and partition pruning shrink that bill.
    • freshet submit straight into a dagron run graph
    • Pay as you go: a platform fee plus usage. No seat or project caps.
    Start on dagron Cloud

    We reply with a workspace and a FRESHET_STATE_TOKEN.

  2. Self-hosted

    The same state backend, on your infrastructure.

    Your network

    • A multi-tenant state backend you run yourself, behind your own boundary
    • Support from the team that builds the planner
    • Bring your own compute
  3. Open source

    The whole planner, uncapped.

    Apache-2.0

    • Every surface above, with nothing gated
    • State in a state.json you commit to git
    • --budget as a guardrail you set for yourself, not a paywall

Know what rebuilds before dbt finishes parsing.