diff --git a/cli/build.mdx b/cli/build.mdx index d934d4e..456246a 100644 --- a/cli/build.mdx +++ b/cli/build.mdx @@ -1,11 +1,14 @@ --- title: "build" -description: "Execute the build lifecycle: compile, plan, and build what changed." +description: "Compile, plan, and execute the selected build lifecycle." --- # sqb build -Compiles, plans, and executes the build lifecycle. By default, SQLBuild runs your full selection. In a [virtual environment](/concepts/virtual-environments), pass `--changes-only` (or set `changes_only = true` in config) to skip work that is already current - unchanged models, seeds, audits, and Python nodes. Use `--no-tests` and `--no-audits` to skip validation for fast iteration. +Compiles, plans, and executes the selected build lifecycle. Direct mode runs the full selected +scope. [Virtual environments](/concepts/virtual-environments) can additionally use +`--changes-only` to build work not already represented by the target VDE's bound versions. Use +`--no-tests` and `--no-audits` to skip validation for fast iteration. ## Usage @@ -18,7 +21,7 @@ sqb --project-dir build [flags] | Flag | Description | |------|-------------| | `--target` | Build against a configured target instead of the active/default target | -| `--changes-only` | Skip models, seeds, audits, and Python nodes that are already current; build only stale work (virtual environments only) | +| `--changes-only` | Virtual mode only: build work not already bound to its expected VDE version | | `--no-tests` | Skip SQL unit tests | | `--no-audits` | Skip audits | | `--no-python` | Skip read-side Python tasks and assets (loader-side Python still runs for selected sources) | @@ -58,7 +61,7 @@ This replaces the former `sqb run` command. The full lifecycle (tests + audits) 2. Seeds are loaded (if changed) 3. Source audits run before their dependent models (unless `--no-audits`) 4. SQL unit tests run before their target model (unless `--no-tests`) -5. Models are materialized in DAG topological order (`--changes-only` skips models already current) +5. Models are materialized in DAG topological order (virtual `--changes-only` limits this to stale work) 6. Error-severity audits run against the staging table before promotion to the target (unless `--no-audits`) ## Output diff --git a/cli/plan.mdx b/cli/plan.mdx index 9b5eeac..c15ec93 100644 --- a/cli/plan.mdx +++ b/cli/plan.mdx @@ -19,7 +19,7 @@ sqb --project-dir plan [flags] |------|-------------| | `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) | | `--no-cache` | Bypass the reusable compile-analysis cache for this invocation | -| `--changes-only` | Narrow the plan to only stale models; prune anything already current (virtual environments only) | +| `--changes-only` | Virtual mode only: show work not already bound to its expected VDE version | | `--no-python` | Exclude read-side Python tasks and assets from the plan | | `--defer-to` | Resolve unselected model references against another target | | `--json` | Output the plan as JSON | diff --git a/concepts/planning.mdx b/concepts/planning.mdx index 1707a3f..779fff1 100644 --- a/concepts/planning.mdx +++ b/concepts/planning.mdx @@ -1,118 +1,138 @@ --- -title: "Overview" -description: "How SQLBuild decides what to build: fingerprints, change reasons, and warehouse-native state." +title: "Planning and change detection" +sidebarTitle: "Overview" +description: "How SQLBuild explains build work, detects changes, and chooses safe model actions." --- -When you run `sqb plan` or `sqb build`, SQLBuild compiles your project, compares it against the current warehouse state, and produces a plan. By default, SQLBuild runs your full selection - the same predictable behavior as a plain build, with nothing to configure. +`sqb plan` previews the work for a build without executing it. `sqb build` uses the same planner +before running the selected resources. -Change-aware pruning is opt-in and requires [virtual environments](/concepts/virtual-environments). In a virtual environment, pass `--changes-only` (or set `changes_only = true` in config) to narrow the run to only stale work - unchanged models, seeds, audits, and Python nodes are then skipped. The fingerprints and change reasons below are recorded on every successful build regardless, so change detection is ready the moment you enable pruning. +Planning and selection are separate concerns: -## What is tracked +- **Selection** determines which resources are in scope. By default, that is the whole project; use + `--select` and `--exclude` to choose a smaller scope. +- **Change detection** compares compiled resources with recorded warehouse state. It explains why a + resource needs work, chooses the correct action for incremental models, propagates upstream + changes, and warns when a partial selection would be incoherent. -Every node in the graph has a versioned identity stored in `_sqlbuild_fingerprints` in the target schema. The planner reads these on every run and compares them against the compiled project. +In the default **direct mode**, a build runs its selected scope. Change detection still matters: the +plan can show query and configuration differences, calculate replay or full-refresh actions, and +identify stale upstreams. It does not filter a direct build to stale models. -### Models and functions - -Each model and function has a **fingerprint** derived from: - -- **Query hash** - the normalized SQL after macro expansion and reference resolution. -- **Config hash** - version-identity config values (materialization settings, contracts, ordered rendered SQL hooks, Python hook invocations and version hashes, custom config/placeholders). -- **Function hashes** - for models that depend on user-defined functions, the function's own fingerprint is included. A function change cascades to all dependent models. +[Virtual environments](/concepts/virtual-environments) additionally bind each model to a versioned +physical relation. Virtual builds can use those bindings to reuse existing versions or limit an +invocation to stale work. That virtual execution behavior is documented under +[Building in a virtual environment](/concepts/virtual-environments/building#stale-driven-execution). -### Seeds +## Reading a plan -Seeds are fingerprinted by content hash and load-affecting config. Unchanged seeds are not reloaded. +Run the plan command with the same target and selectors you intend to build: -### Python nodes +```bash +sqb plan +sqb --target dev plan --select +fact_orders +``` -Loaders, tasks, assets, checks, and Python hooks are fingerprinted by source-code hash, transitive project-dependency hashes (scoped to the git root, so third-party package changes don't count), and decorator config. A Python hook's version hash is also included in every model that invokes it. Reusable SQL hooks are compiled into each consuming model, so their rendered statements participate directly in model identity. +A plan answers four questions: -Python identity tracking is primarily a **visual indicator** in the plan: when a node's identity changes, the plan shows source and dependency diffs. Unlike SQL models, the framework can't observe a Python node's external inputs (an API, a file, a service), so skip/run decisions are **user-controlled** via `ctx.skip()` - the node's own logic decides whether it needs to run. See [Python node pruning](#python-node-pruning). +1. Which resources are selected? +2. What does SQLBuild know about their current warehouse state? +3. Why does each selected model need work? +4. What materialization or incremental action will the build take? -### Audits +Plans may include query diffs, schema changes, backfill ranges, source freshness signals, cascade +reasons, and warnings about stale dependencies. Planning does not modify model relations. -Audits that already passed for the same model version identity are not re-run. When a model's version changes, its audits are re-validated. +## What SQLBuild compares -## Change reasons +### Models and functions -The plan assigns a reason to each node that needs work: +Each model and function has a version identity derived from the inputs that affect its result: -| Reason | Meaning | -|--------|---------| -| First run | No fingerprint exists in the target schema | -| Query changed / checksum changed | The model's query SQL differs from the stored fingerprint (the plan can show a query diff) | -| Config changed | Version-identity config values differ | -| Schema changed | Upstream schema changes detected (column additions, removals, type changes) | -| Upstream changed | An upstream model's change cascades downstream (see [Cascade propagation](/concepts/planning/cascade-propagation)) | -| Run despite unchanged | The model is configured to run periodically even without changes (see [Run despite unchanged](#run-despite-unchanged)) | +- normalized SQL after macro expansion and reference resolution; +- materialization settings, contracts, rendered SQL hooks, Python hook versions, and other + version-relevant configuration; +- referenced function identities; and +- upstream version identities where dependency changes must propagate. -By default, every selected node runs regardless of its reason. Under `--changes-only`, nodes with no pending work are pruned and show as current in the plan output. +A function change therefore appears on the function itself and on dependent models. -## Changes-only mode +### Seeds -Change-aware pruning requires virtual environments (`virtual_environments = true`); it is rejected in direct mode. Within a virtual environment, `--changes-only` narrows the scope to only models that are actually stale: +Seeds use a content hash plus load-affecting configuration. SQLBuild can distinguish an unchanged +seed from one whose file or loading behavior changed. -```bash -sqb build --virtual-env pr_123 --changes-only -sqb build --virtual-env pr_123 --select path:models/marts --changes-only -sqb plan --virtual-env pr_123 --changes-only -``` - -To make it the default for a project or target, set it in config instead of passing the flag every run: +### Python nodes -```toml -[settings] -virtual_environments = true -changes_only = true +Loaders, tasks, assets, checks, and Python hooks have identities derived from project-owned source, +transitive project dependencies, and decorator configuration. Python hook identities also +participate in the identity of models that invoke them. -[targets.dev] -changes_only = true -``` +For standalone Python nodes, this identity is primarily planning evidence. SQLBuild cannot observe +arbitrary external inputs such as APIs or files, so a node controls its own no-work decision with +`ctx.skip()` rather than relying on SQL identity alone. -The CLI flag takes precedence, followed by the selected target, explicit local settings, then project settings. When any source enables it, the planner removes models and functions from the selected scope if they have no pending work; models with any change reason, a pending backfill, or a changed upstream source are kept. Sources, seeds, and other non-model resources are always kept. See [Virtual Environments: Building](/concepts/virtual-environments/building). +### Sources -## On this topic +[Source freshness](/concepts/planning/source-freshness) records an observable data version for a +source. A new observation can mark downstream models as affected even when their SQL is unchanged. -- [Cascade propagation](/concepts/planning/cascade-propagation) - how a change signal propagates downstream, and how each materialization type responds. -- [Source freshness](/concepts/planning/source-freshness) - observing whether external source data has actually changed between runs. -- [Selection and staleness](/concepts/planning/selection-and-staleness) - how `--select` interacts with change detection, and the stale warnings that prevent silent partial rebuilds. +## Change reasons -## Run despite unchanged +The plan assigns reasons to explain detected work: -Some models depend on external data that isn't tracked by source freshness, for example a table model that reads from an API-populated staging area. `run_despite_unchanged` forces a model to run periodically even when its version identity hasn't changed. +| Reason | Meaning | +|--------|---------| +| First run | No previous identity exists for the resource | +| Query changed / checksum changed | Compiled SQL or seed content differs from the recorded identity | +| Config changed | Version-relevant configuration differs | +| Schema changed | A relevant warehouse or upstream schema changed | +| Function changed | A referenced function has a different identity | +| Upstream changed | A dependency change propagated to this model | +| Run despite unchanged | A table's explicit policy requires another run despite an unchanged identity | -```sql -MODEL ( - materialized table, - run_despite_unchanged "always", -); -``` +These reasons explain the plan; they are not a promise that direct mode will skip entries labelled +current. Direct builds execute the selected scope. Virtual mode can compare expected identities with +the versions already bound to a VDE and avoid unnecessary version creation. -- **`always`** - run on every build regardless of state. -- **Duration** (e.g. `24h`, `30d`, `90m`) - run if at least the specified time has passed since the model's upstream source freshness was last observed. Requires at least one upstream source with timestamp freshness tracking. +## Cascades and incremental actions -Only table materializations support `run_despite_unchanged`. When triggered, downstream models are also marked as stale. +Changes propagate through the DAG in dependency order. The resulting action depends on the +materialization: -## Python node pruning +- views are recreated when selected for a direct build; +- tables rebuild when selected; +- incremental models use their cursor state, backfill policy, and `replay_on_change` setting to + determine the affected range; and +- a changed upstream can alter the action of downstream models even when their own SQL is unchanged. -When unchanged SQL models are skipped, read-side Python nodes (tasks, assets, checks) that depend on those models are also skipped. Loaders always run regardless of pruning, since they populate sources that the SQL graph depends on. +See [Cascade propagation](/concepts/planning/cascade-propagation) for the detailed materialization +rules. -Python nodes also have their own identity fingerprints: if a node's source code or dependencies change, it runs even if its SQL dependencies haven't. +## Selection and coherence -## Warehouse-native state (direct mode) +SQLBuild reasons about dependencies outside a scoped selection without silently adding them to the +plan. If building the selected resources would use a stale or missing upstream, the planner warns or +blocks instead of presenting an incoherent partial build as current. -In direct mode, all change-tracking state lives in the warehouse as append-only tables in the same schemas as your data: +See [Selection and staleness](/concepts/planning/selection-and-staleness) for closure selectors and +stale-upstream handling. -- **`_sqlbuild_fingerprints`** - version identities for models, functions, seeds, and Python nodes. One row per successful build per identity. -- **`_sqlbuild_source_freshness`** - source freshness observations. One row per successful build per source identity. -- **`_sqlbuild_node_results`** - Python node runtime results (payload, metadata, status, errors). One row per execution per node. +## Recorded state -There is no external state database, no manifest files, and no state machine with transitions that can corrupt. The planner reads the latest row per identity, compares it against the compiled project, and writes new rows after successful builds. Old rows are retained as immutable history. +Direct mode records append-only planning evidence in the warehouse: -State tables are read across all target schemas in the project, so fingerprints and freshness observations resolve consistently regardless of which schema a model targets. +- `_sqlbuild_fingerprints` stores resource identities; +- `_sqlbuild_source_freshness` stores source observations; and +- `_sqlbuild_node_results` stores Python node outcomes. -Use `sqb janitor` to prune old state history rows while retaining the latest per identity. +The planner reads the latest applicable facts and appends new facts after successful work. Virtual +mode stores version bindings and environment-scoped planning state in its configured state backend; +see [Virtual environment setup](/concepts/virtual-environments/setup). -## Virtual environments +## Related topics -Virtual environments store identities and change-tracking state in the VDE state backend (PostgreSQL or DuckDB) rather than in warehouse fingerprint tables, scoped per environment. See [Virtual Environments: Building](/concepts/virtual-environments/building). +- [Cascade propagation](/concepts/planning/cascade-propagation) +- [Source freshness](/concepts/planning/source-freshness) +- [Selection and staleness](/concepts/planning/selection-and-staleness) +- [Building in a virtual environment](/concepts/virtual-environments/building) diff --git a/concepts/planning/source-freshness.mdx b/concepts/planning/source-freshness.mdx index 5c1ffdc..a34f994 100644 --- a/concepts/planning/source-freshness.mdx +++ b/concepts/planning/source-freshness.mdx @@ -1,10 +1,13 @@ --- title: "Source freshness" sidebarTitle: "Source freshness" -description: "Observing whether external source data has actually changed between runs, so downstream models can be skipped when sources are unchanged." +description: "Observe external source changes and propagate them through planning." --- -Source freshness lets SQLBuild observe whether external source data has actually changed between runs. Under [`--changes-only`](/concepts/planning#changes-only-mode), models downstream of unchanged sources are skipped; on a default full build the observations are still recorded so pruning is accurate the next time you enable it. +Source freshness lets SQLBuild observe whether external source data changed between runs. A changed +observation propagates through the dependency graph, so plans can explain which downstream models +are affected and choose the correct incremental action. In a virtual environment, the same signal +also participates in [stale-driven execution](/concepts/virtual-environments/building#stale-driven-execution). ## Configuration @@ -26,8 +29,12 @@ For timestamp-based freshness, `lag_tolerance` controls how much the observed va ## State storage -Source freshness observations are stored in `_sqlbuild_source_freshness` in each target schema. Records are appended only after the affected downstream models build successfully. If a build fails, the previous observation is preserved so the next run still sees the source as changed. +In direct mode, source freshness observations are appended to `_sqlbuild_source_freshness` in each +target schema. In virtual mode, observations are scoped to the VDE in the configured state backend. +In both modes, failed work does not replace the prior successful observation, so the next plan still +sees the pending source change. -Observations are resolved across all target schemas in the project, so a source referenced by models in different schemas is tracked consistently. +Direct observations are resolved across all target schemas in the project, so a source referenced by +models in different schemas is tracked consistently. Use [`sqb freshness`](/cli/freshness) to observe source freshness on demand without triggering a build. diff --git a/concepts/project-configuration.mdx b/concepts/project-configuration.mdx index 478a5c5..756a998 100644 --- a/concepts/project-configuration.mdx +++ b/concepts/project-configuration.mdx @@ -294,7 +294,7 @@ default_audit_run_scope = "final" | Field | Default | Description | |-------|---------|-------------| | `sql_analysis` | `true` | Enable SQL syntax analysis, column binding, type inference, output inference, and semantic validation at compile time | -| `changes_only` | `false` | Enable [change-aware pruning](/concepts/planning#changes-only-mode) for `plan` and `build` without passing `--changes-only` each run. Requires `virtual_environments = true`; rejected in direct mode. Can also be set per target under `[targets.]`. The CLI flag takes precedence, then the selected target, then local settings, then this project setting. | +| `changes_only` | `false` | In virtual mode, limit `plan` and `build` to [stale-driven execution](/concepts/virtual-environments/building#stale-driven-execution) without passing `--changes-only`. Rejected in direct mode. Can also be set per target under `[targets.]`. The CLI flag takes precedence, then the selected target, local settings, and this project setting. | | `virtual_environments` | `false` | Enable [virtual environments](/concepts/virtual-environments) (versioned model outputs, promotion, rollback, state management). When `false`, the project runs in direct mode. | | `query_change_tracking` | `true` | Track query fingerprints for change detection | | `column_contract_mode` | `implicit` | Controls whether column declarations on models without a `contract` declaration activate static shape/nullability validation. `implicit` preserves that validation; `explicit` treats columns as metadata and audit attachment unless the model declares `contract enforced`. Model-level `contract enforced` and `contract none` override this setting. Explicit type enforcement remains independent. See [Contracts](/concepts/models/contracts). | diff --git a/concepts/sources.mdx b/concepts/sources.mdx index c120210..4fbcad0 100644 --- a/concepts/sources.mdx +++ b/concepts/sources.mdx @@ -110,7 +110,10 @@ For common ingestion you can declare the source entirely in YAML, with no `@load ## Source freshness -Source freshness lets SQLBuild observe whether a source's data has changed between runs. This feeds into [planning and change detection](/concepts/planning): under `--changes-only` (in virtual environments), models downstream of unchanged sources are skipped. +Source freshness lets SQLBuild observe whether a source's data changed between runs. This feeds into +[planning and change detection](/concepts/planning): changed observations propagate to downstream +models and can alter their planned action. Virtual environments can also use the signal for +[stale-driven execution](/concepts/virtual-environments/building#stale-driven-execution). Configure freshness per source with a `freshness:` block: diff --git a/concepts/virtual-environments/building.mdx b/concepts/virtual-environments/building.mdx index d3eb068..051c0f0 100644 --- a/concepts/virtual-environments/building.mdx +++ b/concepts/virtual-environments/building.mdx @@ -71,7 +71,9 @@ Build a subset of models with `--select`: sqb build --virtual-env pr_123 --select fact_orders ``` -Partial builds leave the VDE in `active` (working) status if downstream models remain stale. A follow-up `sqb build --virtual-env pr_123 --changes-only` (without `--select`) builds the remaining stale models to finalize the VDE; a plain `sqb build --virtual-env pr_123` rebuilds the whole selection. +Partial builds leave the VDE in `active` (working) status if downstream models remain stale. A +follow-up `sqb build --virtual-env pr_123 --changes-only` without `--select` builds the remaining +stale models to finalize the VDE. A plain `sqb build --virtual-env pr_123` runs the whole selection. ### Stale upstream coherence @@ -88,17 +90,58 @@ Pass `--include-stale-upstreams` to expand the selection to the minimal set of s sqb build --virtual-env pr_123 --select fact_orders --include-stale-upstreams ``` -### Stale-driven selection +## Stale-driven execution -Virtual environment builds run the full selection by default, like direct mode. Add `--changes-only` to intersect the selection with the stale-driven set, so only models that are both selected and stale are built: +Virtual environment builds run the full selection by default. Add `--changes-only` to limit a plan +or build to work whose expected version is not already bound in the target VDE: ```bash +sqb plan --virtual-env pr_123 --changes-only +sqb build --virtual-env pr_123 --changes-only sqb build --virtual-env pr_123 --select path:models/marts --changes-only ``` -This is useful when the stale cascade is large and you want to build a coherent subgraph without running unchanged models. Without `--changes-only`, every selected model is built regardless of state. +With selectors, SQLBuild intersects the selected scope with the stale set. Models with changed SQL or +configuration, pending backfills, changed functions, or affected upstreams remain in the plan; +models already bound to their expected version do not need another physical version. -Change-aware pruning is opt-in within virtual environments and unavailable in direct mode. See [Planning and Change Detection](/concepts/planning) for how fingerprints, source freshness, and identity tracking determine what gets built. +`--changes-only` is a virtual-mode execution option. Direct mode always runs its selected build +scope, although its plan still reports change reasons and uses them to choose incremental actions. +See [Planning and change detection](/concepts/planning) for the shared identity and cascade model. + +You can make stale-driven execution the default for virtual plans and builds: + +```toml +[settings] +virtual_environments = true +changes_only = true + +[targets.dev] +changes_only = true +``` + +The CLI flag takes precedence, followed by the selected target, local settings, and project +settings. + +### Run despite unchanged + +`run_despite_unchanged` lets a table remain eligible during stale-driven execution even when its +version identity is unchanged. This is useful when external data changes cannot be represented by +source freshness. + +```sql +MODEL ( + materialized table, + run_despite_unchanged "always", +); +``` + +- `always` runs whenever the model is selected. +- A duration such as `24h`, `30d`, or `90m` keeps the model eligible while the newest timestamp data + version from an upstream source is no older than that duration; it is not a periodic scheduler. + +Only table materializations support this setting. When it triggers, downstream models are also +stale. ## Stale detection diff --git a/feature-comparison.mdx b/feature-comparison.mdx index 6b94925..0bf5655 100644 --- a/feature-comparison.mdx +++ b/feature-comparison.mdx @@ -59,7 +59,7 @@ SQLBuild, dbt, and SQLMesh are all SQL pipeline frameworks. They share common gr | Feature | SQLBuild | dbt | SQLMesh | |---------|----------|-----|---------| -| Change-aware builds | Opt-in (`--changes-only`) in virtual environments; fingerprints models, seeds, functions, Python nodes and skips unchanged work including audits | dbt State (paid) | Version hash comparison | +| Stale-driven virtual builds | Optional `--changes-only` execution over VDE version bindings | dbt State (paid) | Version hash comparison | | Warehouse-native state | Append-only tables in the warehouse; no external state database | manifest.json artifacts | Requires external state store (SQLite/PostgreSQL) | | Source freshness | `sqb freshness` with adapter/column/sql strategies, lag tolerance, and CI gating | `dbt source freshness` | No dedicated freshness command; `signals` gate model evaluation until external data is ready | | Reuse across environments | Virtual environments reuse fingerprint-matched physical tables across environments (shared physical storage) | dbt State clone (paid) | Virtual environments reuse fingerprint-matched physical tables across environments (shared physical storage) | diff --git a/index.mdx b/index.mdx index 3ccc7cf..2e670ed 100644 --- a/index.mdx +++ b/index.mdx @@ -55,22 +55,29 @@ Before any model runs, SQLBuild does static analysis of your project - offline, - **Fast, because it's Rust where it counts.** Static analysis runs on [Polyglot](https://github.com/tobilg/polyglot), a Rust SQL engine (MIT, 32+ dialects), so compile stays quick even on large projects. - **Open, not paywalled.** The static analysis is part of the Apache-2.0 core - no proprietary engine, no separate login, and no paid tier gating the smart checks. -## Change-aware builds, when you need them +## Plans that explain every build **Start simple.** By default, `sqb build` runs your full selection - the same predictable mental model as dbt, with nothing to configure. For many projects, that is all they will ever need. -**Scale deliberately.** Change-aware builds are powerful: every model, seed, UDF, and Python node has a versioned identity, so SQLBuild can skip anything already current and only pay for the work that actually changed. But that power introduces complexity - warehouse state to reason about, staleness and cascade behavior, and partial-selection edge cases - that is not worth it for smaller or simpler pipelines. So it is opt-in and part of [virtual environments](/concepts/virtual-environments): turn it on with `--changes-only` (or `changes_only = true` in config) when the cost of full rebuilds outgrows the simplicity of running everything. +Every model, seed, function, and Python node has a versioned identity. SQLBuild uses those identities +to explain query and configuration changes, propagate upstream changes, choose incremental replay +actions, and protect scoped builds from stale dependencies. Direct mode still runs the selected +scope; change detection makes that plan explicit rather than silently changing what was selected. -When enabled, change detection covers the whole graph: +[Virtual environments](/concepts/virtual-environments) add version reuse and optional stale-driven +execution when a project needs isolated previews, promotion, and rollback. -- **Models and UDFs:** fingerprinted by query hash, config, and upstream UDF hashes. Unchanged models are skipped. -- **Seeds:** content and load-affecting config are hashed. Unchanged seeds are not reloaded. -- **Audits:** audits that already passed for the same model version are not re-run. -- **Source freshness:** external source data versions are tracked automatically. Models downstream of unchanged sources are skipped, with lag tolerance to avoid jitter. +The planner carries that evidence across the whole graph: + +- **Models and functions:** query, configuration, and dependency identities explain local and cascaded changes. +- **Seeds:** content and load-affecting configuration identify when a seed changed. +- **Audits:** audit evidence is associated with the model version it validated. +- **Source freshness:** observable source data versions can affect downstream plans, with lag tolerance to avoid jitter. - **Cascade propagation:** when a model does change, the signal propagates downstream, with configurable replay windows (`replay_on_change`). -- **Python nodes:** loaders, tasks, assets, checks, and hooks are fingerprinted by source and dependency hashes; skip/run is user-controlled via `ctx.skip()`. +- **Python nodes:** source and dependency identities appear in plans; skip/run remains user-controlled through `ctx.skip()`. -State is plain append-only rows in your own warehouse (`_sqlbuild_fingerprints`, `_sqlbuild_source_freshness`, `_sqlbuild_node_results`) - no external state database, no manifest files, no state machine that can corrupt. +In direct mode, this evidence is stored as append-only rows in your warehouse. Virtual environments +use their configured state backend for environment-scoped version bindings. ## Deploy reversibly (opt-in)