Skip to content

fix(test): isolate unit-test SQL per model and reserve helper names - #986

Merged
kvlonge merged 10 commits into
mainfrom
fix/unit-test-sql-isolation
Oct 6, 2026
Merged

kvlonge merged 10 commits into
mainfrom
fix/unit-test-sql-isolation

Conversation

@kvlonge

@kvlonge kvlonge commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Why

SQL unit tests that chain models could read the wrong relation. Model CTEs were hoisted to the top level of the test query, so two models that both end in final (or a model CTE and a test helper with the same name) shared one name. On warehouses where an outer CTE wins over a nested redefinition, such as Snowflake, a model silently read another model's or a helper's rows. Models referenced by several steps were also inlined repeatedly.

Changes

  • Each model under test is inlined once as __ref__<model> with its own CTEs nested; __actual__<model>, later steps and assertions reuse it.
  • Test helpers become reserved top-level names (__helper__<name>); references in fixtures, mocks, expectations and assertions are rewritten through the parsed SQL, so models never see helper names and bare table names read the real table. Without SQL analysis, helpers stay inline in the fixtures that read them. SQL Server keeps hoisting with renaming.
  • A test CTE that redefines a helper name is a compile error instead of warehouse-dependent behaviour.
  • Helper CTEs are emitted once.
  • A trailing ; on a model no longer breaks later steps or assertions.
  • --json-output reports every parameterized case and each test's duration_ms.

Verification

  • Structural Rust test on every supported dialect: top-level and nested CTE names never overlap.
  • DuckDB E2E for chained models sharing CTE names, a helper named like a physical table (analysis on and off), trailing ;, parameterized JSON cases and duration_ms.
  • Generated test SQL checked read-only on Snowflake: every step and assertion reports zero mismatches.
  • One independent review plus follow-up; make rust-check, make test, make check-ci.

SQL unit tests hoisted the CTEs of the model under test into the test's
top-level WITH for `__actual__` and also nested the model again as its
`__ref__` CTE. Every model was emitted twice, and a hoisted name such as
`final` could resolve inside a later step's nested WITH on warehouses
that look up outer CTE names first.

Each planned model step is now defined once as `__ref__<model> AS
(<model SQL>)`, its own CTEs stay nested, and `__actual__<model>` reads
`SELECT * FROM __ref__<model>`. Only T-SQL, which rejects a nested WITH,
still flattens model CTEs, renaming collisions as before.
Every mock CTE carried a copy of every test-defined helper CTE, so a
helper such as `expected_rows` used only by an assertion was repeated in
each fixture. Mocks now contain only their own SQL, and the helpers a
mock reads are defined once in the test's top-level WITH. A mock inlined
into model SQL, where a model CTE could shadow a top-level helper, still
carries only the helpers it reads.
`sqb test --json-output` checks had no timing, although the text output
shows one per test. Each SQL test check now reports `duration_ms` from
the same lifecycle record, matching the unit used by JSON assets.
JSON output looked up SQL test results by test name only, so parameterized
cases, whose events carry the path, block and case identity, were dropped
from checks. Use the shared sql_test_resource_id for both.
Helpers placed at the top level of the test query could be read by a nested
model CTE or bare table of the same name on warehouses where outer CTEs win
or are ambiguous. With analysis, helpers now get reserved __helper__<name>
CTE names and test-authored readers are rewritten through the AST; without
analysis they stay inline in the fixtures that read them. Rendering refuses
any nested CTE that reuses a top-level name, a test CTE that redefines a
helper is a compile error, and model bodies drop a trailing terminator in
planning so downstream steps share one definition.
A WINDOW clause entry such as `base AS (ORDER BY id)` was read as a CTE,
so an assertion with a named window sharing a helper's name was refused.
Only commas directly inside a WITH list now separate CTE definitions.
@kvlonge
kvlonge enabled auto-merge (squash) October 6, 2026 18:26
…-test SQL shape

The guards hash every compiled artifact, including compiled unit-test SQL,
whose shape changed intentionally (models nested once under __ref__).
Compile JSON and model artifacts are unchanged.
@kvlonge
kvlonge merged commit 0e38e08 into main Oct 6, 2026
36 checks passed
@kvlonge
kvlonge deleted the fix/unit-test-sql-isolation branch October 6, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant