Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ All state is persisted as append-only tables in the warehouse alongside your dat
- **Cursor-based incremental processing.** Automatic gap detection and resume, with microbatch mode for large ranges. No external checkpoint to maintain.
- **Ingestion and Python nodes.** Load external data with Python `@loader` functions, and run `@task`, `@asset`, and `@check` nodes as first-class members of the same DAG as your SQL models.

See the [documentation](https://docs.sqlbuild.com) for the full feature set, including providers, lifecycle hooks, Python macros, UDFs, custom materializations, data diffs, zero-copy cloning, and virtual environments. To coordinate dbt and SQLBuild projects, see the [dbt compatibility guide](https://docs.sqlbuild.com/concepts/dbt-compatibility/overview).
See the [documentation](https://docs.sqlbuild.com) for the full feature set, including providers, lifecycle hooks, Python macros, UDFs, custom materializations, data diffs, zero-copy cloning, and virtual environments. This repository also includes a focused guide to [model and raw-query diffs](docs/query-diffs.md). To coordinate dbt and SQLBuild projects, see the [dbt compatibility guide](https://docs.sqlbuild.com/concepts/dbt-compatibility/overview).

## Quick start

Expand Down
120 changes: 120 additions & 0 deletions docs/query-diffs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Query and model data diffs

`sqb diff` compares either selected models or two literal read-only SQL queries. Both input modes
use the same schema inspection, row-count reporting, key validation, deterministic sampling,
numeric tolerance, excluded-column, evidence, JSON, and exit-code behavior.

## Compare models

Model mode keeps the `FROM:TO` range and selectors:

```bash
sqb diff prod:dev --full --select orders
```

By default, row identity comes from each model's `unique_key`. Repeated `--key` arguments override
that configuration for one invocation:

```bash
sqb diff prod:dev --full --select order_lines \
--key order_id \
--key line_id
```

For a model without a stable row key, opt in to exact full-row multiset comparison with
`--unkeyed`. Duplicate rows retain their multiplicity; SQLBuild never guesses a key.

```bash
sqb diff prod:dev --full --select daily_order_totals --unkeyed
```

## Compare raw queries

Query mode omits `FROM:TO` because each query identifies its own input. Supply each side inline or
from a UTF-8 file, and choose keyed or unkeyed comparison explicitly:

```bash
sqb diff \
--left-query 'SELECT order_id, amount_cents FROM analytics.orders_before' \
--right-query 'SELECT order_id, amount_cents FROM analytics.orders_after' \
--key order_id
```

```bash
sqb diff \
--left-query-file queries/orders_before.sql \
--right-query-file queries/orders_after.sql \
--key account_id \
--key order_id
```

Raw inputs must contain exactly one read-only `SELECT`, `WITH`, or `VALUES` expression. SQLBuild
does not expand `ref()` or other SQLBuild templates in these inputs. The active target connection
executes both sides; use `--target NAME` to select another configured connection.

Query mode performs a full comparison when no comparison mode flag is present. `--schema-only` is
also available. `--bounded` is model-specific; filter both raw queries directly when a bounded
query comparison is needed.

### Sampling, tolerances, and exclusions

Keyed query comparisons support deterministic union-key sampling and the same bounded mismatch
examples as model comparisons:

```bash
sqb diff \
--left-query-file queries/orders_before.sql \
--right-query-file queries/orders_after.sql \
--key order_id \
--sample-rows 50000 \
--sample-seed 7
```

Use repeated invocation overrides where needed:

```bash
sqb diff \
--left-query-file queries/orders_before.sql \
--right-query-file queries/orders_after.sql \
--key order_id \
--exclude-column loaded_at \
--tolerance amount_cents:absolute=1 \
--tolerance conversion_rate:relative=0.001
```

`--exhaustive` disables inherited or requested sampling. Unkeyed comparison is always exhaustive
and exact, so it does not accept sampling or numeric tolerances.

## Stable query results and cleanup

SQLBuild materializes each raw query once in the selected target's default schema before comparing
it. Artifacts use a reserved run-owned name and are dropped immediately when the command finishes,
including after comparison errors.

Each successfully created artifact publishes an immutable `query_diff_artifact` fingerprint. The
fingerprint is ownership evidence for crash recovery only: running and comparing the queries never
reads historical fingerprints, and fingerprints do not represent planned, running, complete, or
cleaned workflow states.

If a process stops before immediate cleanup, the next query diff and `sqb janitor` can remove an
expired artifact only when its exact physical identity and reserved name match its ownership
fingerprint. A matching name without ownership evidence is reported for manual investigation and
is not deleted automatically.

The recovery TTL defaults to 24 hours and accepts SQLBuild fixed-duration syntax:

```toml
[diff]
query_artifact_ttl = "24h"
```

The TTL is a crash-recovery backstop, not the normal cleanup path. Historical ownership inspection
problems are reported but do not prevent the current queries from running. Failure to publish
ownership for a newly created artifact, or failure to remove a current-run artifact, is an execution
error.

## Structured output and exit behavior

Use `--json-output PATH` in either mode. Query results include `input_kind = "query"`; model results
use `input_kind = "model"`. Exit code `0` means no differences were found. Exit code `1` means
differences were found or the comparison could not execute successfully.
102 changes: 90 additions & 12 deletions src/sqlbuild/.agents/skills/sqlbuild/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7753,10 +7753,12 @@ Compare schemas and data between targets or virtual environments to validate cha

SQLBuild can compare schemas and row-level data between two build contexts. This lets you validate that changes produce the expected results before promoting them.

`sqb diff FROM:TO` compares:
`sqb diff` supports two input modes:

- **two targets** (e.g. `prod:dev`) in direct mode, or
- **two virtual environments** (VDEs) when [virtual environments](/concepts/virtual-environments) are enabled.
- **model mode:** `sqb diff FROM:TO --select ...` compares two targets (for example `prod:dev`) in
direct mode, or two virtual environments when virtual environments are enabled;
- **raw-query mode:** inline SQL or SQL files identify the two sides and execute through the active
target connection or an explicit `--target`.

The mechanics below are identical for both; only what `FROM` and `TO` refer to changes.

Expand All @@ -7770,9 +7772,30 @@ for that execution connection still fail before warehouse inspection.
sqb diff prod:dev --full --select customer_status_snapshot
```

Raw-query mode omits `FROM:TO` and requires repeated `--key` values or explicit `--unkeyed` for a
row comparison. Inputs must each be one literal read-only `SELECT`, `WITH`, or `VALUES` expression;
SQLBuild templates such as `ref()` are not expanded.

```bash
sqb diff \
--left-query 'SELECT order_id, amount_cents FROM analytics.orders_before' \
--right-query 'SELECT order_id, amount_cents FROM analytics.orders_after' \
--key order_id

sqb diff \
--left-query-file queries/orders_before.sql \
--right-query-file queries/orders_after.sql \
--target dev \
--key account_id \
--key order_id
```

Query mode defaults to a full comparison and also supports `--schema-only`. It does not accept
`--bounded`; put the desired filter in both SQL inputs instead.

### Comparison modes

Every diff requires exactly one mode:
Model mode requires exactly one mode. Raw-query mode defaults to full when no mode is supplied:

#### Full diff

Expand Down Expand Up @@ -7864,7 +7887,9 @@ side.

### Row matching

Rows are matched between the two sides using the model's `unique_key`. Models without a `unique_key` can use schema-only diff but cannot run full or bounded row comparisons.
Rows are matched using the model's `unique_key` or repeated invocation-level `--key` overrides.
For inputs without stable row identity, `--unkeyed` performs an exhaustive exact multiset
comparison in which duplicate rows retain their multiplicity. SQLBuild never guesses a key.

The diff output categorises rows as:
- **Equal** - same key, same values on both sides
Expand Down Expand Up @@ -7896,6 +7921,10 @@ Tolerance rules support:

Tolerances can be set per-column (`by_column`) or per-type (`by_type`).

Use repeated `--tolerance COLUMN:absolute=VALUE` or `--tolerance COLUMN:relative=VALUE` arguments
to override per-column rules for one model or raw-query invocation. Unkeyed comparison is exact and
does not accept tolerances.

### Excluded columns

Columns that are expected to differ between the two sides (like timestamps or context-specific values) can be excluded from the row comparison:
Expand All @@ -7910,6 +7939,29 @@ MODEL (

Excluded columns are still shown in the schema comparison but skipped during row-level diffing. A column cannot be in both `row_diff_exclude_columns` and `unique_key`.

Use repeated `--exclude-column COLUMN` arguments for invocation-level exclusions.

### Raw-query artifacts and crash recovery

SQLBuild materializes each raw query once in the active target schema, compares the stable results,
and drops both artifacts in a `finally` path. Every successfully created artifact appends an
immutable `query_diff_artifact` fingerprint containing ownership and expiry metadata. Query
execution and comparison never depend on historical fingerprints; they are ownership evidence for
later cleanup, not workflow status.

Before a query diff, and independently through `sqb janitor`, SQLBuild removes an expired artifact
only when its exact physical identity and reserved name match its fingerprint. A reserved-name
relation without matching ownership evidence is reported and retained for manual investigation.
Configure the crash-recovery TTL, which defaults to 24 hours, in `sqlbuild_project.toml`:

```toml
[diff]
query_artifact_ttl = "24h"
```

Historical ownership inspection failures are warnings and do not prevent the current query pair
from running. Current-run ownership publication and immediate cleanup remain mandatory.

### Verbose output

Add `--verbose` or `-v` to see more example rows for mismatches and side-only rows:
Expand Down Expand Up @@ -7955,7 +8007,7 @@ observed schema for each model before starting its row comparison.

### Selectors

Diff requires `--select` in the current version. You can use any selector syntax:
Model diff requires `--select` in the current version. You can use any selector syntax:

```bash
# Diff a single model
Expand All @@ -7970,7 +8022,9 @@ sqb diff prod:dev --full --select tag:acceptance

### Exit codes

`sqb diff` returns exit code `0` when all selected models have no differences, and `1` when any model has schema or row differences. This makes it usable in CI pipelines as a validation gate.
`sqb diff` returns exit code `0` when no differences are found, and `1` when any input has schema or
row differences or the comparison cannot execute successfully. This makes it usable in CI pipelines
as a validation gate.

## Execution Observability

Expand Down Expand Up @@ -14448,23 +14502,30 @@ destination. Managed physical sources may bootstrap a destination that has not b

Source: `cli/diff.mdx`

Compare schemas and data between targets or virtual environments.
Compare schemas and data between targets, virtual environments, or two literal read-only queries.

Compares schemas and optionally row-level data between two build contexts: two targets (e.g. `prod:dev`) in direct mode, or two virtual environments when virtual mode is enabled. See [Data Diffs](/concepts/diff) for detailed usage.
Compares schemas and optionally row-level data between two build contexts, or materialized results
from two literal read-only SQL inputs. See [Data Diffs](/concepts/diff) for detailed usage.

### Usage

```bash
sqb diff <FROM>:<TO> <mode> [flags]
sqb diff (--left-query SQL | --left-query-file PATH) \
(--right-query SQL | --right-query-file PATH) (--key COLUMN... | --unkeyed) [flags]
```

The first argument is a positional `FROM:TO` range. Exactly one mode is required: `--full`, `--schema-only`, or `--bounded <duration>`.
Model mode uses a positional `FROM:TO` range and requires exactly one of `--full`, `--schema-only`,
or `--bounded <duration>`. Raw-query mode does not accept `FROM:TO`; it defaults to full comparison,
also supports `--schema-only`, and does not support `--bounded`.

In direct mode, `FROM` and `TO` are configured target names. Their database/schema namespaces remain
authoritative, while the `TO` target's named connection executes the complete comparison and must
be able to read both namespaces.

Full and bounded row comparisons require the model to define `unique_key`. Bounded mode uses the model's cursor and falls back to a full row comparison when no cursor is configured.
Full and bounded keyed comparisons use the model's `unique_key` unless repeated `--key` arguments
override it. `--unkeyed` opts into exact multiset comparison. Bounded mode uses the model's cursor
and falls back to a full row comparison when no cursor is configured.

### Flags

Expand All @@ -14482,6 +14543,15 @@ Full and bounded row comparisons require the model to define `unique_key`. Bound
| `--json-output` | Write structured comparison scope, coverage, and results to a JSON file |
| `--max-models` | Fail when the selected scope contains more models than this limit |
| `--max-columns` | Fail when either side of a model has more columns than this limit |
| `--left-query` | Literal read-only SQL for the left side |
| `--left-query-file` | UTF-8 SQL file for the left side |
| `--right-query` | Literal read-only SQL for the right side |
| `--right-query-file` | UTF-8 SQL file for the right side |
| `--target` | Target connection for raw-query mode; defaults to the active target |
| `--key` | Row identity column; repeat for a composite key or to override model configuration |
| `--unkeyed` | Compare exact full-row multiplicities without a key |
| `--exclude-column` | Column to omit from row comparison; repeat as needed |
| `--tolerance` | Per-column numeric rule such as `amount:absolute=0.01`; repeat as needed |
| `--no-sql-analysis` | Disable compile-time SQL analysis (`--no-sql-validation` is an alias) |
| `--select`, `-s` | Select specific models to diff (required in v1) |
| `--exclude` | Exclude specific models from diffing |
Expand All @@ -14503,11 +14573,19 @@ sqb diff prod:dev --bounded 14d --sample-rows 50000 --sample-seed 7 --select ord

# Force exhaustive comparison despite inherited sampling defaults
sqb diff prod:dev --bounded 14d --exhaustive --select order_lines

# Compare two raw queries with a composite key
sqb diff \
--left-query-file queries/orders_before.sql \
--right-query-file queries/orders_after.sql \
--key account_id \
--key order_id
```

### Exit codes

Returns `0` when all selected models have no differences, `1` when any model has schema or row differences.
Returns `0` when no differences are found. Returns `1` when any input has schema or row differences
or the comparison cannot execute successfully.

## lineage

Expand Down
Loading
Loading