Skip to content

Replay case setup on DuckDB: tables, session variables and user-defined types (D228 part 2) - #130

Closed
kvlonge wants to merge 15 commits into
mainfrom
feat/d228-setup-replay
Closed

kvlonge wants to merge 15 commits into
mainfrom
feat/d228-setup-replay

Conversation

@kvlonge

@kvlonge kvlonge commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

D228, part 2: verify replays a case's setup on DuckDB. Part 1 (the case format and recorder contract) made every setup case refuse. This replays the setup statements that have receipts and still refuses the rest.

What setup replays

Setup runs statement by statement, in order, and the whole case refuses at the first statement it can't replay:

  • CREATE TABLE / INSERT INTO … VALUES: an unqualified, unquoted table with typed NUMBER and VARCHAR columns, created once, holding literals or NULL that each column holds without rounding. Relations resolve through {{run_database}} / {{run_schema}} and unqualified names.
  • SET / UNSET of session variables (rule.session_variable): to a literal, NULL or a deterministic scalar subquery. $name reads it in either case. A variable setup didn't SET, an UNSET of one it never set, an expression, and a subquery whose result can change between calls (RANDOM, the clock) all refuse. DuckDB computes a subquery's value at each read, while Snowflake computes it once at SET, so a CREATE TABLE or INSERT while such a variable is still set refuses (review round 1).
  • CREATE TYPE name AS data_type: a type spelled as that plain name, in either case, resolves to its base type in casts, typing and later setup columns. That's what Snowflake's receipts report: a::age is NUMBER(3,0), and a + b over two age columns is NUMBER(4,0). Quoted names, redefinitions, OR REPLACE, IF NOT EXISTS, qualified names and non-alias definitions refuse. So does a built-in type's name: one SQLQuack converts, or one that doesn't parse as a custom type, such as GEOGRAPHY (review round 1).

Not replayed (still refuse)

  • CREATE SEMANTIC VIEW / SEMANTIC_VIEW(…): the parser keeps CREATE SEMANTIC VIEW only as raw text (node:raw). SEMANTIC_VIEW(…) stores METRICS and DIMENSIONS in separate fields, so it loses the clause order that decides column order (semantic-view t002). Both need parser work before a rule can be receipt-backed.
  • ALTER SESSION and CREATE VIEW in setup: no case uses either, so no receipt backs a rule.
  • A cast to a structured type (data-types-user-defined t003, ::address over OBJECT(…)) was already refused, and still is.

Brought in from land/d151-6n2b (ruling D306)

rule.session_variable needs a passing case, so this PR carries the session-variables cases and their receipts, byte for byte, and nothing else from that branch: no recorder, no other cases, no obligations.

Commit here From Paths
5e3a016 C's 487be66 (author and date kept) cases/translation/expressions/session-variables.toml
7439a34 A's 7a9436d (author and date kept) evidence/results/{13884601cc86…,24b0b834ddee…,bbd0b6d5c294…,c41db8d3b834…,fbd897b012ca…}.{json,arrow}, the receipts of t004, t003, t001, t005 and t002

All 11 blobs are identical to their source commits' (git rev-parse <commit>:<path>). The evidence guard passes, with 10 added result files. The five cases are uncited until C lands their obligations in obligations/expressions.toml, which are left out here. A and C have been told these paths land through this PR.

Evidence

In a scratch merge with land/d151-6n2b, the 21 setup cases give:

File Passed Refused No receipt
data-types-user-defined t001, t002, t004 (100046 overflow), t005 t003 (structured cast)
name-resolution t001, t002, t003, t005 t004
session-variables t001–t005
semantic-view t001–t004, t006 t005

That's 13 passed and 0 failed. Unit tests in test_case_setup cover each accepted and refused shape, including:

  • a quoted "age" that must not resolve, an alias of an alias, and a setup column typed by an alias
  • a write after a subquery SET, which gives 2 instead of Snowflake's 1 without the round-1 fix
  • the same write after an UNSET, which replays

SEMANTIC VIEW is a separate post-launch item (D306).

Gate

Gate (D94) on e56c888, which merges main de14f81 and is 98bdfd0 minus the baseline re-record:

  • make check-merge exits 0: clippy, fensu (0 faults) and all test binaries pass.
  • verify --require-verified --strict-errors: 12,012 cases, 7,858 passed, 1,032 diverged as expected, 3,085 refused, 37 typed, 0 pending, 0 unproven, 0 failed. 221 of 221 equivalences and rules are verified, including rule.session_variable.
  • translation-results --against main de14f81: 0 regressions and 11 changes, all gains. sf.expressions.session-variables goes Unmeasured → Matching (59 → 60 of 1,183 forms), its five obligations are met, and its five statements match.
  • 98bdfd0 only re-records coverage/translation/results-baseline.json from that run. It checks clean against itself.
  • The session-variables obligations are C's 5840035, cherry-picked as 9d24a0d with an identical blob. The auditor approved them (record a8dc7de).
  • Main has since moved to 78910c3 (D267: statistics aggregates, ADD_MONTHS, MONTHS_BETWEEN and NVL2 from receipts #127). This PR re-merges and re-gates when it gets its slot. The auditor noted a conflict with BINARY: the type, the VARCHAR→BINARY cast and byte-wise text functions (D284) #129 in catalogue/constants.rs and the baseline.

Draft, for the auditor. GitHub Actions is blocked by billing, so the local D94 gate counts.

Case setup of CREATE TABLE with NUMBER and VARCHAR columns and INSERT
INTO it VALUES of literals now converts: the tables join the case's
schema and their DuckDB statements run before the query, with the
run database and schema placeholders read as DuckDB's memory.main.
Every other setup statement still refuses, as do constraints,
defaults, quoted names and values a column would round.

Incomplete: SET variables, CREATE TYPE and SEMANTIC VIEW, the
ALTER SESSION profile overlay, docs, and a full verify are not done.
Checked: the new setup test only.
conversion/constants.rs is at 2,000 lines on main, and the BINARY rules
add to it. The 601-line table of time zone names Snowflake was recorded
accepting now lives in time_zones/constants.rs. No behaviour change.
…part 2, slice 2)

SET of a variable, or of a list of them, to a literal, NULL or a scalar
subquery is recorded during setup, and $name reads the expression it was
set to (rule.session_variable). Subqueries reading variables or calling
functions whose result can change between the SET and the read refuse.
Main merged in; the recorded time zone table moved out of
conversion/constants.rs.

Incomplete: fensu faults in convert_setup.rs (split needed), CREATE TYPE,
SEMANTIC VIEW, ALTER SESSION, docs, full verify. The new rule has no case
on main until the 6n2b cases land. Checked: setup tests; the 6n2b
session-variable and name-resolution cases replay on a scratch merge.
convert_setup keeps only the statement loop; CREATE TABLE and INSERT
live in _helpers/setup/tables.rs, SET and UNSET in variables.rs, and
the helpers return what they read instead of mutating the setup.
fensu reports no faults. No behaviour change.
…ts base type (D228 part 2)

Setup records each CREATE TYPE name AS data_type. When the tree is
built, a custom type spelled as a plain name that setup created is
replaced by its base type, so casts, typing and setup CREATE TABLE
columns all see the base type, as Snowflake's receipts report it.
Quoted names, redefinitions, OR REPLACE, IF NOT EXISTS, qualified
names, built-in type names and non-alias definitions refuse.
…(D306)

Only cases/translation/expressions/session-variables.toml, which
rule.session_variable needs to verify; its obligations stay with C's
later landing.
…(D306)

The ten result files of the five session-variables cases, as A recorded
them; nothing else from that commit.
DuckDB recomputes the subquery at each $name read, while Snowflake
computed it once at SET time, so a later CREATE TABLE or INSERT could
change the value. Such a write now refuses while the variable is set.
A user-defined type's name must also parse as a custom type, so
GEOGRAPHY and other names SQLQuack does not convert refuse too.
SET of one and of two variables read in a BETWEEN, a number and a text
variable, case-insensitive names, a variable set from a query, and a NULL
column beside a variable.
… cases (D228)

sf.expressions.session-variables moves from unmeasured to matching, with
its five obligations met and five statements matching; nothing else
changes.

kvlonge commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing: SQLQuack lands through local-gate trains, not PR merges (D94), so open PRs only add clutter (D628/D630). The branch is kept. Any work here that isn't on main yet is tracked by the train owner and lands through a train.

@kvlonge kvlonge closed this Nov 8, 2026
@kvlonge
kvlonge deleted the feat/d228-setup-replay branch November 9, 2026 21:31
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