Repository navigation
fix: Snowflake, scenario, rule and type correctness batch - #1250
Merged
Merged
Conversation
Planning rebinds models against the source columns it lists from the
warehouse. The Snowflake adapter lists column names lowercased, and the
rebinding stored them quoted ("bundle_sku"), so an ordinary unquoted
reference, which Snowflake resolves as BUNDLE_SKU, failed with B002
unless the session set QUOTED_IDENTIFIERS_IGNORE_CASE.
Plain single-case names are now stored unquoted on Snowflake, so they
bind the way Snowflake resolves unquoted names. Mixed-case or special
names stay quoted.
BREAKING CHANGE: on Snowflake without QUOTED_IDENTIFIERS_IGNORE_CASE, a
quoted lowercase reference such as "bundle_sku" to a listed source column
now reports B002 during planning, as Snowflake would reject it against an
upper-case column. The listing cannot tell a column really created with a
quoted lowercase name apart, so such columns now bind as unquoted names.
Row-returning managed-source loaders rendered VARIANT values as PARSE_JSON(...) inside the VALUES list, but Snowflake allows only constants there, so those loads failed. JSON values are now written as string constants and parsed with PARSE_JSON in the projection over the VALUES rows. Snowflake string literals treat backslashes as escapes, so the adapter now doubles them when quoting loader strings. Without that, JSON escapes such as \" and any backslash in a loaded string were altered.
The local snapshot fingerprint hashed fixture SQL after the running adapter rendered it. Capture on Snowflake and replay on DuckDB render a partial fixture's wrapper alias in different case, and capture can add casts from warehouse column types that offline planning does not, so a snapshot was reported stale (X602) right after it was captured, and capturing again did not help. Fixture plans now carry the authored fixture SQL, and the fingerprint uses it, so capture and local replay agree. BREAKING CHANGE: local scenario snapshots captured by earlier versions report as stale once. Run `sqb scenario capture` to refresh them.
The fixture checker's column-read walk counted lambda parameters, such as i in FILTER(items, i -> ...) and acc and q in (acc, q) -> acc + q, as unqualified column reads. When one relation in scope was read through a star, those names resolved to it and the scenario failed with S510 asking the mock to supply them. Unqualified references to a lambda's own parameters are now skipped in projections and in clauses. Compile lineage and column-reference rewriting already scoped lambda parameters. DuckShop's cart funnel scenario drops its placeholder acc, i and q mock columns.
…e schemas A contract-enforced model whose query selected a star was treated as having a non-authoritative schema, so `__empty_fixture()` mocks of it were rejected (S510), and so were the `__empty_fixture()` fixtures `sqb format` writes for typed `WHERE FALSE` fixtures of contracted models. An enforced contract fixes the model's output columns, so its declared schema is now authoritative whatever the query looks like.
SQBRSQL004 flagged `LIMIT 0` without ORDER BY, though it selects no rows in any order. Empty fixtures written as `SELECT ... LIMIT 0` tripped it.
…ed types A fixture's bare NULL columns are cast to the mocked relation's declared types, but only when the fixture was a single SELECT. A fixture written as a UNION ALL of rows kept untyped NULLs, so typed functions over them failed, for example YEAR(NULL) on Snowflake or DuckDB. Set operations now take their NULL columns from the first branch, which names the output.
…urces When a model failed, an end-of-build audit that read it still ran and failed with "object does not exist". A SQL test that called a table function skipped because of the same failure also ran: failure blocking only reached selected models and functions, not tests in the execution order. Both are now skipped, like the audits attached to a skipped model.
…window A scenario rebuilds every model it runs from scratch, and a non-microbatch full rebuild has no cursor interval, so a scenario that ran an incremental model calling __cursor_start() or __cursor_end() failed. Such models now run over the same wide window SQL tests use (1900-01-01 to 2999-12-31, or the integer equivalent), so mocked rows are not filtered out. Microbatch models keep discovering their range from their inputs.
A built-in relationships audit written as `to "fct_orders"` compiled
without complaint, but it queried a bare, unqualified table name and did
not depend on the model, so it could run before that table existed, or
read another one. The documentation showed that form. The `to` argument
must now be a `__ref(...)`, `__source(...)`, `__seed(...)` or
`__dbt_ref(...)` call, and compile says which reference to write.
BREAKING CHANGE: a built-in `relationships` audit whose `to` is a plain
or quoted relation name is now a compile error (P001). Write
`to __ref("name")` (or `__source` / `__seed`) instead.
Without --select, a build's default selection included every managed source and its Python loader, so `sqb build --no-load` and `auto_load_sources = false` still ran the loaders. With loading off, the default selection now leaves managed sources and their loaders out. A source selected by name still loads. BREAKING CHANGE: `sqb build --no-load`, or a build with `auto_load_sources = false`, no longer runs managed-source loaders unless the source is selected explicitly.
A sized TEXT(n) or STRING(n) normalised as an unrelated type, so a contract declaring VARCHAR(n) rejected a column cast to TEXT(n). Sized variable-length text now normalises as VARCHAR(n) outside BigQuery, and BigQuery spells every variable-length text type STRING. The generated-type golden is re-recorded for the new spellings.
Header scanners and the tokenizer applied the prose quoting rules to any string after a description key, so a meta value such as meta (description "say \", then") ended at the escaped quote and the header failed to parse. Prose rules now apply only to description keys outside a meta value.
…he cast SQBRCONTRACT105 compared an outer cast's type with the declared contract type by spelling, so on Snowflake CAST(... AS VARCHAR(16777216)) or CAST(... AS STRING) was rejected against a VARCHAR contract. Both types are now normalized for the target dialect before comparing. A mismatched outer cast now names the cast's type and the declared type, points at the CAST itself, and asks to change the cast rather than wrap the expression in another one. Model rule locations were relative to the query below the header, so they pointed at header lines. They are now lines of the model file.
sqb format laid out only a header's own description: one line when it fits, otherwise an indented triple-quoted block. Column and audit descriptions now get the same layout at their own nesting depth. Formatting is idempotent and stored text is unchanged.
SQL rules expand a test file before linting it, and @PARAM("name") is not a macro, so every parameterised test failed with L005 while a SQBRSQL rule was selected. Test parameter references now stand as string sentinels for linting, as audit parameters already do.
The fixture checker read LEVEL, CONNECT_BY_ISLEAF and CONNECT_BY_ISCYCLE in a CONNECT BY query as unqualified input columns. When a relation in scope was read through a star, they resolved to it and the scenario failed with S510 asking the mock for them. They now read no input in a SELECT that has a CONNECT BY clause.
A macro mock had to be a bare SELECT of one string literal, which SQBRSQL022 rejects as a calculated projection with no stable name, so every macro mock needed a rule exception. The literal may now carry an AS alias, which is ignored. DuckShop's mock uses one and drops its exception.
SQBRTEST102 asked a test file to be named after each of its blocks' behaviours, which no file holding two differently named blocks can satisfy. A file of several blocks now needs only the test_<subject>__<behavior>.sql grammar. DuckShop drops its exception.
SQL macro argument types were parsed only for four adapters, so a Postgres argument of type FOO BAR or a MotherDuck INTEGER NOT NULL compiled silently, and argument decimals were never range-checked. Argument types now go through the same per-dialect check as returns. BigQuery NUMERIC and BIGNUMERIC types beyond their integer-digit and scale limits, such as NUMERIC(40, 2), are now reported too. Adds an E2E for BigQuery STRING contracts filled by sized VARCHAR casts. BREAKING CHANGE: SQL macros whose argument types the adapter's dialect cannot parse or hold, and BigQuery NUMERIC or BIGNUMERIC types wider than BigQuery allows, now fail to compile with P015.
Removes the limitations this batch fixes: unquoted Snowflake column references after rebinding, JSON loader rows, managed sources reloaded with loading off, cursor-intrinsic incremental models and empty fixtures in scenarios, stale snapshots, untyped NULL source fixtures, tests and audits over skipped resources, and macro tests of folder-scoped macros.
…very branch Casting a set-operation fixture's bare NULL columns to the declared types also cast columns that a later branch already typed, changing that branch's values. Only columns that are a bare NULL in every branch are now cast; a typed value in any branch types the column as before.
…yout and BigQuery STRING Model rule findings now carry file lines, column descriptions are laid out like the model's, BigQuery compares VARCHAR and STRING as one type, and builds that should run loaders opt in to loading sources.
# Conflicts: # tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py
A select-list item that was one call to a macro with returns passed SQBRCONTRACT105 without a cast. returns is analysis-only and never reaches the generated SQL, so a body such as 'ACTIVE' created a VARCHAR(6) column under a VARCHAR contract with no error. A call now establishes the contract type only when the generated SQL casts it to that type: the macro body ends in CAST(... AS type) or ...::type, or the call is cast. Otherwise the rule names the macro and its returns type and asks for a cast in the macro or at the call. returns TYPE OF calls follow the same rule. BREAKING CHANGE: a contracted output that is one call to a SQL macro with returns, whose body is not cast to the contract type, now fails SQBRCONTRACT105 until the body or the call is cast.
…rage version Local scenario replay created its DuckDB file at DuckDB's default storage version, v1.0.0, which cannot hold VARIANT columns, so a captured snapshot with a VARIANT column, such as a PARSE_JSON fixture on Snowflake, failed with X699. The local database is sqlbuild's own and recreated on every run, so it is now opened with storage_compatibility_version = latest. The DuckDB adapter passes that connection option through; existing database files keep their own storage version and still open.
…xture The local snapshot fingerprint hashed only a fixture's own authored text, so editing a helper CTE the fixture reads, such as changing helper_ids from SELECT 1 to SELECT 2, left a stale snapshot looking fresh. The fingerprint now also covers the authored body of every helper CTE the fixture reads, transitively, and whether each relation reference in them reads a scenario fixture or the warehouse. All of it is authored, adapter-neutral text, so a snapshot captured on one adapter stays fresh when replayed on another. BREAKING CHANGE: local scenario snapshots captured by earlier versions report as stale once. Run sqb scenario capture to refresh them.
…ng is off With loading off, the previous fix dropped managed sources from the default selection along with their loaders, so their audits stopped running and a build over bad source data passed. Loading off now means only that no loader runs: managed sources stay in the plan without a load step, and their audits check the data they already hold. This holds for --no-load and auto_load_sources = false, and now also with --select, where naming a managed source used to run its loader anyway. Audits of a selected source that loads nothing and that no model read during the build run at the end of the build. BREAKING CHANGE: with --no-load or auto_load_sources = false, no managed-source loader runs, even for a source selected by name. Selected managed sources are audited against their existing data, so a build over failing source data now fails.
An end-of-build audit that read a resource the build did not build was dropped from the results without a trace. It is now recorded with a new skipped audit outcome and the reason, such as "reads broken_orders, which was not built", shown on its progress row, in the skipped rollup, in the JSON checks and in the SKIP count. Skipped audits are not written to audit result history. A model counted as unbuilt whenever it failed or was skipped, including a model a Python hook skipped on purpose, whose relation still holds earlier data. Only a model that was not built and has no existing relation now counts as unbuilt, so the end-of-build audits over a hook-skipped model run.
Loader string values on Snowflake now have every backslash escaped, so values are stored exactly as returned. Documents the effect on keys loaded by earlier releases. BREAKING CHANGE: Snowflake loader string values containing backslashes are now stored literally. Earlier releases wrote them unescaped, so merge, delete_insert or append deduplication keys containing backslashes will not match rows those releases loaded.
…onfig The DuckDB adapter read storage_compatibility_version from the connection config, which made it a user setting for project warehouses. The latest storage version now applies only to the local scenario replay database, through an internal adapter method, and a user config naming the key ignores it like any other unknown key.
End-of-build source audits ran for every source no model read, on every build, so a plain build over an unmanaged source with a missing table failed. They now run only when loading is off (--no-load or auto_load_sources = false), or for a source selected by name. With loading on and no such selection, a source is audited only before a model that reads it, as before. An unbuilt model is now one that was not built and whose relation the inspected warehouse does not hold, rather than one planned as a first run, so a full refresh on a fresh warehouse skips the end audits over a failed model.
An end-of-build audit skipped because it reads a resource the build did not build has passed: false in the JSON checks, so Dagster reported it as a failed asset check, even in a green build. The Dagster integration now emits no check result for a skipped audit, which Dagster shows as not evaluated, and logs the audit and its skip reason instead. The JSON shape is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Correctness gaps found while running the DuckShop benchmark on DuckDB and Snowflake.
Changes
returnsalone no longer establishes a contract type; casts compare per dialect.LIMIT 0is deterministic; model rule lines are file lines; parameterised tests lint; SQBRTEST102 accepts multi-block files.sqb formatlays out column and audit descriptions.BREAKING CHANGE: quoted lowercase Snowflake source column references, relationships
towithout__ref, macro argument types the dialect rejects, over-wide BigQuery NUMERIC and uncast typed macro calls on contracts fail to compile; loading off skips every loader, even for selected sources, and audits their data; Snowflake loader backslashes are stored literally; local snapshots report stale once.Verification
make test,make check-ci,make compiler-differentialpass.BEGIN_COMMIT_OVERRIDE
fix!: on Snowflake, listed source columns bind as unquoted identifiers, so a quoted lowercase reference such as "bundle_sku" now reports B002, as Snowflake would reject it
fix!: relationships audits require
toto be a relation reference (__ref,__source,__seedor__dbt_ref), otherwise P001fix!: a typed SQL macro call no longer satisfies a contract on
returnsalone; the macro body or the call must cast to the contract type (SQBRCONTRACT105)fix!: SQL macro argument types the adapter's dialect can't parse or hold, and BigQuery NUMERIC/BIGNUMERIC wider than the dialect allows, fail with P015
fix!: with loading off (
--no-loadorauto_load_sources = false) no loader runs, even for selected sources, and their audits check the existing datafix!: Snowflake loader string values keep backslashes literally, so dedupe keys containing backslashes won't match rows loaded by earlier versions
fix!: local scenario snapshots are fingerprinted from the authored fixture SQL, its helper CTEs and reference targets, so existing snapshots report stale once
fix: Snowflake, scenario, rule and type correctness batch (lambda parameters and CONNECT BY pseudo-columns are not mock columns; contracted models are authoritative fixture schemas; local replay supports VARIANT; NULL-only set-operation fixture columns are cast; skipped tests and end audits are reported with a reason; LIMIT 0 is deterministic; model rule findings use file lines; sized TEXT and STRING compare as VARCHAR; meta descriptions and description layout)
END_COMMIT_OVERRIDE