Skip to content

Commit 134fe07

Browse files
committed
feat: add raw query diff inputs
1 parent 7577ebc commit 134fe07

52 files changed

Lines changed: 2719 additions & 63 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ All state is persisted as append-only tables in the warehouse alongside your dat
2424
- **Cursor-based incremental processing.** Automatic gap detection and resume, with microbatch mode for large ranges. No external checkpoint to maintain.
2525
- **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.
2626

27-
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).
27+
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).
2828

2929
## Quick start
3030

‎docs/query-diffs.md‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Query and model data diffs
2+
3+
`sqb diff` compares either selected models or two literal read-only SQL queries. Both input modes
4+
use the same schema inspection, row-count reporting, key validation, deterministic sampling,
5+
numeric tolerance, excluded-column, evidence, JSON, and exit-code behavior.
6+
7+
## Compare models
8+
9+
Model mode keeps the `FROM:TO` range and selectors:
10+
11+
```bash
12+
sqb diff prod:dev --full --select orders
13+
```
14+
15+
By default, row identity comes from each model's `unique_key`. Repeated `--key` arguments override
16+
that configuration for one invocation:
17+
18+
```bash
19+
sqb diff prod:dev --full --select order_lines \
20+
--key order_id \
21+
--key line_id
22+
```
23+
24+
For a model without a stable row key, opt in to exact full-row multiset comparison with
25+
`--unkeyed`. Duplicate rows retain their multiplicity; SQLBuild never guesses a key.
26+
27+
```bash
28+
sqb diff prod:dev --full --select daily_order_totals --unkeyed
29+
```
30+
31+
## Compare raw queries
32+
33+
Query mode omits `FROM:TO` because each query identifies its own input. Supply each side inline or
34+
from a UTF-8 file, and choose keyed or unkeyed comparison explicitly:
35+
36+
```bash
37+
sqb diff \
38+
--left-query 'SELECT order_id, amount_cents FROM analytics.orders_before' \
39+
--right-query 'SELECT order_id, amount_cents FROM analytics.orders_after' \
40+
--key order_id
41+
```
42+
43+
```bash
44+
sqb diff \
45+
--left-query-file queries/orders_before.sql \
46+
--right-query-file queries/orders_after.sql \
47+
--key account_id \
48+
--key order_id
49+
```
50+
51+
Raw inputs must contain exactly one read-only `SELECT`, `WITH`, or `VALUES` expression. SQLBuild
52+
does not expand `ref()` or other SQLBuild templates in these inputs. The active target connection
53+
executes both sides; use `--target NAME` to select another configured connection.
54+
55+
Query mode performs a full comparison when no comparison mode flag is present. `--schema-only` is
56+
also available. `--bounded` is model-specific; filter both raw queries directly when a bounded
57+
query comparison is needed.
58+
59+
### Sampling, tolerances, and exclusions
60+
61+
Keyed query comparisons support deterministic union-key sampling and the same bounded mismatch
62+
examples as model comparisons:
63+
64+
```bash
65+
sqb diff \
66+
--left-query-file queries/orders_before.sql \
67+
--right-query-file queries/orders_after.sql \
68+
--key order_id \
69+
--sample-rows 50000 \
70+
--sample-seed 7
71+
```
72+
73+
Use repeated invocation overrides where needed:
74+
75+
```bash
76+
sqb diff \
77+
--left-query-file queries/orders_before.sql \
78+
--right-query-file queries/orders_after.sql \
79+
--key order_id \
80+
--exclude-column loaded_at \
81+
--tolerance amount_cents:absolute=1 \
82+
--tolerance conversion_rate:relative=0.001
83+
```
84+
85+
`--exhaustive` disables inherited or requested sampling. Unkeyed comparison is always exhaustive
86+
and exact, so it does not accept sampling or numeric tolerances.
87+
88+
## Stable query results and cleanup
89+
90+
SQLBuild materializes each raw query once in the selected target's default schema before comparing
91+
it. Artifacts use a reserved run-owned name and are dropped immediately when the command finishes,
92+
including after comparison errors.
93+
94+
Each successfully created artifact publishes an immutable `query_diff_artifact` fingerprint. The
95+
fingerprint is ownership evidence for crash recovery only: running and comparing the queries never
96+
reads historical fingerprints, and fingerprints do not represent planned, running, complete, or
97+
cleaned workflow states.
98+
99+
If a process stops before immediate cleanup, the next query diff and `sqb janitor` can remove an
100+
expired artifact only when its exact physical identity and reserved name match its ownership
101+
fingerprint. A matching name without ownership evidence is reported for manual investigation and
102+
is not deleted automatically.
103+
104+
The recovery TTL defaults to 24 hours and accepts SQLBuild fixed-duration syntax:
105+
106+
```toml
107+
[diff]
108+
query_artifact_ttl = "24h"
109+
```
110+
111+
The TTL is a crash-recovery backstop, not the normal cleanup path. Historical ownership inspection
112+
problems are reported but do not prevent the current queries from running. Failure to publish
113+
ownership for a newly created artifact, or failure to remove a current-run artifact, is an execution
114+
error.
115+
116+
## Structured output and exit behavior
117+
118+
Use `--json-output PATH` in either mode. Query results include `input_kind = "query"`; model results
119+
use `input_kind = "model"`. Exit code `0` means no differences were found. Exit code `1` means
120+
differences were found or the comparison could not execute successfully.

‎src/sqlbuild/.agents/skills/sqlbuild/SKILL.md‎

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7753,10 +7753,12 @@ Compare schemas and data between targets or virtual environments to validate cha
77537753

77547754
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.
77557755

7756-
`sqb diff FROM:TO` compares:
7756+
`sqb diff` supports two input modes:
77577757

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

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

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

7775+
Raw-query mode omits `FROM:TO` and requires repeated `--key` values or explicit `--unkeyed` for a
7776+
row comparison. Inputs must each be one literal read-only `SELECT`, `WITH`, or `VALUES` expression;
7777+
SQLBuild templates such as `ref()` are not expanded.
7778+
7779+
```bash
7780+
sqb diff \
7781+
--left-query 'SELECT order_id, amount_cents FROM analytics.orders_before' \
7782+
--right-query 'SELECT order_id, amount_cents FROM analytics.orders_after' \
7783+
--key order_id
7784+
7785+
sqb diff \
7786+
--left-query-file queries/orders_before.sql \
7787+
--right-query-file queries/orders_after.sql \
7788+
--target dev \
7789+
--key account_id \
7790+
--key order_id
7791+
```
7792+
7793+
Query mode defaults to a full comparison and also supports `--schema-only`. It does not accept
7794+
`--bounded`; put the desired filter in both SQL inputs instead.
7795+
77737796
### Comparison modes
77747797

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

77777800
#### Full diff
77787801

@@ -7864,7 +7887,9 @@ side.
78647887

78657888
### Row matching
78667889

7867-
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.
7890+
Rows are matched using the model's `unique_key` or repeated invocation-level `--key` overrides.
7891+
For inputs without stable row identity, `--unkeyed` performs an exhaustive exact multiset
7892+
comparison in which duplicate rows retain their multiplicity. SQLBuild never guesses a key.
78687893

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

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

7924+
Use repeated `--tolerance COLUMN:absolute=VALUE` or `--tolerance COLUMN:relative=VALUE` arguments
7925+
to override per-column rules for one model or raw-query invocation. Unkeyed comparison is exact and
7926+
does not accept tolerances.
7927+
78997928
### Excluded columns
79007929

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

79117940
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`.
79127941

7942+
Use repeated `--exclude-column COLUMN` arguments for invocation-level exclusions.
7943+
7944+
### Raw-query artifacts and crash recovery
7945+
7946+
SQLBuild materializes each raw query once in the active target schema, compares the stable results,
7947+
and drops both artifacts in a `finally` path. Every successfully created artifact appends an
7948+
immutable `query_diff_artifact` fingerprint containing ownership and expiry metadata. Query
7949+
execution and comparison never depend on historical fingerprints; they are ownership evidence for
7950+
later cleanup, not workflow status.
7951+
7952+
Before a query diff, and independently through `sqb janitor`, SQLBuild removes an expired artifact
7953+
only when its exact physical identity and reserved name match its fingerprint. A reserved-name
7954+
relation without matching ownership evidence is reported and retained for manual investigation.
7955+
Configure the crash-recovery TTL, which defaults to 24 hours, in `sqlbuild_project.toml`:
7956+
7957+
```toml
7958+
[diff]
7959+
query_artifact_ttl = "24h"
7960+
```
7961+
7962+
Historical ownership inspection failures are warnings and do not prevent the current query pair
7963+
from running. Current-run ownership publication and immediate cleanup remain mandatory.
7964+
79137965
### Verbose output
79147966

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

79568008
### Selectors
79578009

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

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

79718023
### Exit codes
79728024

7973-
`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.
8025+
`sqb diff` returns exit code `0` when no differences are found, and `1` when any input has schema or
8026+
row differences or the comparison cannot execute successfully. This makes it usable in CI pipelines
8027+
as a validation gate.
79748028

79758029
## Execution Observability
79768030

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

1444914503
Source: `cli/diff.mdx`
1445014504

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

14453-
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.
14507+
Compares schemas and optionally row-level data between two build contexts, or materialized results
14508+
from two literal read-only SQL inputs. See [Data Diffs](/concepts/diff) for detailed usage.
1445414509

1445514510
### Usage
1445614511

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

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

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

14467-
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.
14526+
Full and bounded keyed comparisons use the model's `unique_key` unless repeated `--key` arguments
14527+
override it. `--unkeyed` opts into exact multiset comparison. Bounded mode uses the model's cursor
14528+
and falls back to a full row comparison when no cursor is configured.
1446814529

1446914530
### Flags
1447014531

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

1450414574
# Force exhaustive comparison despite inherited sampling defaults
1450514575
sqb diff prod:dev --bounded 14d --exhaustive --select order_lines
14576+
14577+
# Compare two raw queries with a composite key
14578+
sqb diff \
14579+
--left-query-file queries/orders_before.sql \
14580+
--right-query-file queries/orders_after.sql \
14581+
--key account_id \
14582+
--key order_id
1450614583
```
1450714584

1450814585
### Exit codes
1450914586

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

1451214590
## lineage
1451314591

0 commit comments

Comments
 (0)