Cross-cut verification matrix for ADR-0032 Phase 2 is now fully populated with concrete test references — every row green. Filling the matrix surfaced three real gaps that this commit closes. 1. Advanced-mode syntax highlighting (ADR-0030 §8 matrix row). The `ui.rs` Advanced branch routed through `plain_input_spans`, bypassing the highlight walker entirely. In production SQL keywords past the entry word rendered as plain identifiers. Fix: mode-aware variants of `highlight_runs`, `render_input_runs`, `lex_to_runs`, and `input_diagnostics`; the Advanced render path now uses the highlighted form with `Mode::Advanced`. `plain_input_spans` removed (unused). 2. Engine.* key wiring (ADR-0032 §11.4 / §13 matrix rows + handoff §3.3 follow-up). The four Phase-2 engine.* catalog entries were authored in 2d but never reached: `translate_generic` discarded the engine message and returned a vague catalog entry. Fix: pattern-match the engine message text for the four Phase-2 categories (aggregate misuse, group-by required, compound arity mismatch fallback, scalar-subquery cardinality) inside `translate_generic`, routing each to its engine-neutral catalog entry. 3. Matrix-coverage tests. Thirteen new tests covering the rows that had no explicit coverage: - 3 SQL keyword/operator/CASE highlight tests - 4 engine.* engine-message tests - 3 sql_expr column-completion tests (WHERE, HAVING) - 3 predicate-warning slot tests (CASE, ORDER BY, projection) - 1 all-10-playground-types recovery test (tests/sql_select.rs) Plan document (docs/plans/20260520-adr-0032-phase-2.md) updated: every (TBD) row in the cross-cut matrix replaced with a concrete test file::function reference and a green status marker. Test totals: 1428 → 1441 passing (+13 new). Clippy clean.
42 KiB
Implementation plan — ADR-0032 (ADR-0030 Phase 2)
Plan author date: 2026-05-20
Driving ADR: ADR-0032 — The full SQL SELECT grammar
Parent ADR: ADR-0030 — Advanced mode: the standard-SQL surface
Purpose
ADR-0032 is the decision: what Phase 2 covers, how it composes, and what consequences fall out. This plan is the mechanics: the build order, the per-step exit gates, the cross-cut verifications that prevent the kind of silent gap Phase 1 shipped (the SQL-expression predicate-warning gap closed in ADR-0032 §11.6).
The plan does not re-litigate ADR-0032's decisions. Any finding during implementation that suggests a decision should change is escalated to the user and resolved by ADR amendment (or a new ADR) before code proceeds — never by silent drift.
Why this plan exists
Phase 1 of ADR-0030 shipped with a structurally green test suite
(1260 passing, 0 failing) and a believed-but-unverified "ambient
assistance comes for free" claim. The verification missed the
fact that ADR-0027's predicate warnings — emitted by a pass
walking the DSL Expr AST — could not fire on SQL expressions
because sql_expr.rs builds no AST (ADR-0031 §2). ADR-0032
§11.6 documents the gap and closes it; this plan exists to make
sure Phase 2 does not ship with an analogous unnoticed gap.
The two failure modes the plan is built to prevent:
- The "free for free" claim shipping without a test that proves the claim. Every "X works for SQL automatically" in ADR-0030/0031/0032 needs an explicit cross-cut test.
- A sub-phase reporting green because its own scope is covered, while a behaviour it enables for downstream code is silently broken. Each sub-phase's exit gate includes the cross-cut behaviours it unlocks, not just its own internal correctness.
Working-mode reminder
Per CLAUDE.md's "Working mode — solo with sub-agents by
default": this work proceeds in solo mode unless the user
explicitly invokes /team. The phased structure below maps to
solo-mode's Phase 1 → 5 discipline within each sub-phase:
- Requirements extraction (the sub-phase's scope below) is Phase 1.
- Divergent exploration is rarely needed for individual sub-phases here (ADR-0032 already selected the approach), but if implementation reveals an ambiguity, it lands as a Phase-2 exploration before code proceeds.
- The DA hat is worn at every sub-phase gate. The DA critique is written, not silent (CLAUDE.md "Devil's Advocate discipline").
- Escalation rather than guessing applies throughout.
Standing authorizations and continuation policy
The user has granted standing authorizations for this plan,
overriding the corresponding CLAUDE.md defaults for this
plan's scope only. They are recorded here verbatim so any
implementer (the original session, a future session, or a fresh
agent) operates on the same terms.
1. Walk the plan without interruption when no interruption is needed
The plan's sub-phases (2a–2g) have explicit, written exit gates. Within a sub-phase, the implementer proceeds through the build steps and verifies the gate without asking for permission at each step. Between sub-phases, the implementer proceeds to the next one without asking, provided the previous gate is green.
The implementer does NOT interrupt to:
- Confirm "should I continue to the next sub-phase?"
- Confirm minor implementation choices that match surrounding
code (
CLAUDE.md's code-style discipline applies; pick the consistent option and move on). - Confirm test naming, file organisation, commit granularity within a sub-phase.
- Report progress mid-step. A status update arrives at gate boundaries — not every five minutes.
2. When the implementer MUST escalate
The escalation rule from CLAUDE.md ("Escalate ambiguity — do
not decide for the user") is not relaxed. Standing
authorization to walk uninterrupted is not authorization to
guess on design questions. The implementer escalates if:
- An exit gate cannot be made green by work in the current sub-phase's scope — a test fails for a reason that suggests the ADR is wrong, or a behaviour the gate requires turns out to need machinery not in ADR-0032.
- An ambiguity arises that the ADR and this plan do not settle. See the "Open questions to escalate before code starts" section at the end of this plan — those are pre-identified. Anything similar that surfaces is also escalation territory.
- An autonomous design decision is required (a choice outside
ADR-0032 §§1–13's settled list and the plan's per-sub-phase
scope). Per
CLAUDE.md's "Out of scope and non-blocking require user confirmation": the default assumption is that the work is in scope and is blocking unless the user has said otherwise. - A "real blocker" is encountered — environment, dependency,
or test-infrastructure failure that prevents progress and
cannot be resolved within the sub-phase. (A flaky test is
not a real blocker — investigate per
CLAUDE.md's "no excuses" rule on test infrastructure.)
3. Commits are pre-authorized at standard granularity
The user has explicitly pre-authorized commits within this
plan's scope, overriding CLAUDE.md's "All commits MUST be
confirmed by the user first" for this plan. The implementer:
- Commits at natural break points within each sub-phase and always at sub-phase gate boundaries. Granular is better than monolithic.
- Writes detailed commit messages in the project's existing
style —
area: short subjectfirst line, then a body explaining what changed and why. Recent precedent:Body should reference the ADR section being implemented (e.g., "ADR-0032 §10.2 — Node::ScopedSubgrammar variant added") so a reader can trace each commit back to a decision.grammar: SQL SELECT end-to-end (ADR-0030 Phase 1) tests: Phase 1 SQL SELECT integration tests app: mode-threaded completion, overlay, and validity indicator - Includes NO AI attribution of any form (
Co-Authored-By, generated-by lines, AI footers, …). This is a global rule (CLAUDE.md); the standing authorization here is for committing, not for relaxing attribution discipline.
4. Pushes remain a user step
CLAUDE.md's "Push is a user step — agents cannot and shall
never do it" is not overridden. The implementer commits
freely; the user pushes when they choose. Unpushed commits are
a normal working state, and the implementer never describes
work as blocked on a push.
5. End-of-plan handoff
At the end of 2g, after the verification report is produced and the DA's final PASS verdict is recorded, the implementer stops and surfaces a summary to the user — exact test counts, the filled cross-cut matrix, the requirements-to-test mapping, any autonomous decisions made and their justification. The user confirms acceptance before the plan is considered done. (This is the one explicit hand-back point.)
If the user wants follow-up work — adjustments, polish, additional tests — the implementer addresses it and re-runs the gate before re-surfacing. The plan does not declare itself complete; the user does.
Sub-phase overview
| Sub-phase | Title | Test-suite gate |
|---|---|---|
| 2a | Grammar fragment | All §1 productions accept; OOS shapes reject |
| 2b | ScopedSubgrammar + scope accumulators |
Subquery scope isolation; CTE self-reference visible |
| 2c | Phase-1 grammar migration | All 7 Phase-1 SQL SELECT tests still green |
| 2d | Diagnostics passes | Every catalog key has positive + negative tests; Phase-1 gap closure verified |
| 2e | Completion + post-walk fixup | Per-slot completion matrix green; projection-before-FROM scenario verified |
| 2f | Type resolution | Each of 10 playground types preserved through bare ref |
| 2g | Integration sweep + cross-cut verification | All cross-cut tests green; final verification report |
Each sub-phase ships independently: green tests, clippy clean, a written DA gate review, and (per project rules) a user-confirmed commit message before commit.
Sub-phase 2a — Grammar fragment
Scope (in)
- Author
src/dsl/grammar/sql_select.rsexportingpub static SQL_SELECT_STATEMENT: Nodeandpub static SQL_SELECT_COMPOUND: Node. - Encode the full §1 BNF:
with_clause,compound_select,select_core, the projection list withDISTINCT/ALL/*/t.*/bare-alias, thefrom_clause/join_clausechain with the five JOIN flavours,where_clause,group_by_clause,having_clause,order_by_clause,limit_clausewithOFFSET. - Wire
sql_expr::SQL_OR_EXPRinto every expression slot viaSubgrammar. - Author the structural one-line hint key per slot (per ADR-0032 Consequences "Hint-panel prose" bullet); the keys are placed in the catalog with placeholder text. Rich teaching prose is Phase 6.
- Encode the
reject_internal_tablevalidator on every new table-source slot.
Scope (out — explicit)
- The
ScopedSubgrammarvariant — that is 2b. - The Phase-1
data::SELECTmigration — that is 2c. - Any new
WalkContextfield — that is 2b.
Build steps
- Stub
sql_select.rswith the static node skeleton and re-export throughgrammar/mod.rs. - Author each production tier as a named
static Nodein loosest-to-tightest order, matching the §1 BNF. - For each production, add a unit test in the file (the
expr.rs/sql_expr.rstest pattern) that walks a representative input. - Author the OOS reject tests (NATURAL JOIN, comma-FROM,
LIMIT m, n,VALUES,LATERAL, window-functionOVER). cargo testandcargo clippy --all-targets -- -D warnings.
Exit gate
Required green tests:
- Every JOIN flavour (
INNER, bareJOIN,LEFT [OUTER],RIGHT [OUTER],FULL [OUTER],CROSS) accepts. - Every set op (
UNION,UNION ALL,INTERSECT,EXCEPT) accepts in a compound chain. - Both CTE forms (
WITH,WITH RECURSIVE) accept; both column- list and no-column-list shapes accept. DISTINCT/ALLaccepts and rejects each other co-occurring.*,t.*, bare-alias projection,AS aliasprojection all accept.- Qualified column refs (
t.c) accept in WHERE/projection/ ORDER BY/HAVING/GROUP BY. LIMIT nandLIMIT n OFFSET mboth accept;LIMIT m, nrejects.- Every §13 OOS-1 to OOS-7 shape rejects with a parse error (not a silent accept).
Required negative tests: absence of bare RECURSIVE (must
be WITH RECURSIVE); absence of JOIN after INNER (must be
INNER JOIN); etc. — the obvious parse-failure modes for each
production.
Test-suite count: ≥ 50 new unit tests in sql_select.rs
(rough order; the actual count is dictated by production
coverage).
Other gates:
cargo testtotal: 1260 (baseline) + ≥ 50 (new) = ≥ 1310. Zero failures, one ignored (the unchanged doctest), zero skipped.cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does every §1 BNF production have at least one positive test?
- Does every §13 OOS shape have at least one reject test?
- Did the implementation introduce any structure not covered by ADR-0032 §1 or §10–§11 (an autonomous decision)? If yes, escalate to the user before the gate is signed.
- Are the OOS rejects emitting engine-neutral parse errors, or are they leaking SQLite messages?
Sub-phase 2b — ScopedSubgrammar + scope accumulators
Implementation status (2026-05-20)
Sub-phase 2b shipped in five commits (4f89106, 98a74b2,
b522d09, 4ff054c, and earlier 2a foundations). The
scope-accumulator infrastructure — Node::ScopedSubgrammar,
ScopeFrame, from_scope_stack, the new Ident flags
(writes_table_alias, writes_cte_name,
writes_projection_alias), and the sql_expr §5 / §6 additive
extensions — is complete and exercised by 26 driver-level
tests.
Deferral, explicitly user-approved: §10.3 stage 2 (the six
CTE output-column derivation rules) is NOT implemented in 2b.
Stage 1 (placeholder CTE binding push) IS implemented; stage 2
will fold into 2d (where the new arity-check pass needs
declared-vs-derived column counts) and 2e (where qualified-
prefix completion needs CTE columns). Until then, a CTE
binding's columns stays empty after the body exits, and
qualified-prefix completion past cte_alias.| returns an
empty candidate list. The CTE-name is still visible as a
table source from inside the body (WITH RECURSIVE
self-reference works) and from outside (downstream CTE-name
validators see it).
The 2g cross-cut matrix rows for §10.3 derivation cases are deferred along with the harvest; they will land when 2d/2e implements the rules.
Scope (in)
- Add the new walker node variant
Node::ScopedSubgrammar(&Node). - Add the
ScopeFramestruct andfrom_scope_stack: Vec<ScopeFrame>toWalkContext. Addfrom_scope,cte_bindings,projection_aliasesas fields insideScopeFrame. - Teach the walker driver to push/pop a fresh frame on
ScopedSubgrammarentry/exit. - Rewire every Subgrammar reference to
&SQL_SELECT_COMPOUNDinsql_expr.rsandsql_select.rsto use the new variant. - Teach
from_clause/join_clausewalks to populate the current frame'sfrom_scope. - Teach
with_clausewalks to populate the outer frame'scte_bindings(placeholder push pre-body per §10.3 stage 1). - Teach
projection_itemwalks to populate the current frame'sprojection_aliases. - Implement §10.3 stage 2: at the CTE body's frame exit, derive output columns from the body's projection and rewrite the placeholder binding in the outer frame.
- Keep
current_table/current_table_columnsas derived helpers (top-frame single-binding view) — verify the DSL paths still see them correctly.
Scope (out — explicit)
- Qualified-prefix completion narrowing — that is 2e.
- The post-walk fixup pass for projection identifiers — that is 2e.
- The arity-check ERROR pass (uses the frame-exit hook from §10.3 stage 2 but adds a new pass) — that is 2d.
Build steps
- Add the variant to
Node; add walker driver match arm (push, recurse, pop). - Add
ScopeFrameandfrom_scope_stacktoWalkContext; rewritecurrent_table/current_table_columnsaccessors to derive from the top frame. - Rewire Subgrammar references to ScopedSubgrammar in
sql_select.rs(subquery primary, IN/EXISTS tails, CTE bodies — except for the ladder-internal recursions insql_expr.rsthat stay asSubgrammar). - Teach the
from_clause/join_clausewalker handlers to build and pushTableBindings. - Teach the
with_clausehandler to push the placeholderCteBindingbefore entering the body. - Teach the
projection_itemhandler to record aliases. - Implement the body-frame-exit hook that derives CTE output columns per §10.3 stage 2 rules.
- Unit tests for each.
Exit gate
Required green tests:
- An empty query through the walker still produces the baseline output (no spurious push).
- A DSL
UPDATE T …still setscurrent_tablecorrectly via the derived helper (DSL paths untouched). - A subquery's FROM does not leak: parse
SELECT * FROM a WHERE id IN (SELECT id FROM b), then probe a cursor outside the subquery — onlyais infrom_scope. - A correlated reference resolves: parse
SELECT * FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.x = a.x), then probe inside the subquery —ais visible via outer frame. - A CTE name is visible inside its own body: parse
WITH r AS (SELECT 1 UNION ALL SELECT n+1 FROM r) SELECT * FROM r, then probe inside the body —ris incte_bindings. - A CTE body's
SELECT *derives the right output columns: parseWITH x AS (SELECT * FROM users) …, verifyx's derived columns matchusers' columns by name and playground type. - A CTE body's
SELECT a, b AS bb FROM tderives two columns with the right names and types (a: source type;bb: source type since underlying is a bare ref). - A CTE body's
SELECT a + 1 FROM tderives one column withname = None. - A CTE column list
(c1, c2)renames positionally; arity mismatch is detectable at frame exit (the diagnostic is emitted by 2d but the detection runs here). - The depth cap (
MAX_SUBGRAMMAR_DEPTH = 64) still fires on pathological nesting — exercised through both variants.
Other gates:
- All 2a tests still green.
- All 1260 baseline tests still green (DSL paths verified insulated).
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Did any DSL test regress? If yes, the
current_tablehelper is not deriving correctly — fix before gate signs. - Is the
ScopedSubgrammarpush/pop happening on every entry and every exit, including error paths? Read the walker driver's match arm and verify. - Does the CTE column derivation handle all six §10.3 rules? Walk the rule table and find a test for each.
- Did 2b introduce any structure ADR-0032 doesn't sanction?
Sub-phase 2c — Phase-1 grammar migration
Scope (in)
- Move the Phase-1 SQL
SELECTgrammar nodes fromsrc/dsl/grammar/data.rsintosrc/dsl/grammar/sql_select.rs(or remove them if redundant with the new §1 productions). - Rebuild the
data::SELECTCommandNodeso its body is a reference toSQL_SELECT_STATEMENT. - Confirm the seven Phase-1 SQL
SELECTintegration tests still pass without modification.
Scope (out — explicit)
- Any behaviour change. This is a refactor: the Phase-1 surface must be preserved exactly.
Build steps
- Identify the Phase-1 SQL
SELECTstatic nodes indata.rs(SELECT_PROJECTION_*,SELECT_WHERE_*,SELECT_ORDER_*,SELECT_LIMIT_*if separate, plus any sub-tree they reference). - Move them into
sql_select.rsor remove if the §1 productions cover them. - Rewrite the
data::SELECTCommandNodeto referenceSQL_SELECT_STATEMENT. - Run the seven Phase-1 SQL
SELECTintegration tests.
Exit gate
Required green tests:
- All seven Phase-1 SQL
SELECTintegration tests (tests/sql_select_*.rsor equivalent) pass without modification. - All 2a and 2b tests still green.
- All 1260 baseline tests still green.
Other gates:
cargo clippy --all-targets -- -D warningsclean.- Dead code: confirm via
cargo checkthat the olddata.rsPhase-1 SELECT nodes are either removed or re-exported, not orphaned.
DA gate (written)
- Were any Phase-1 SELECT behaviours quietly altered? Diff the Phase-1 integration test outputs (text and structural) — they must be identical, not "equivalent".
- Did the migration produce dead code? Removing dead code is fine; leaving it isn't.
Sub-phase 2d — Diagnostics passes
Scope (in)
- Implement the three diagnostic passes per ADR-0032 §11.7:
- Schema-existence ERROR pass (extended for multi-binding
from_scope, qualified-reference resolution, ambiguity detection). - Arity-check ERROR pass (new, hooked to CTE-body and compound-query frame-exits from 2b).
- Predicate-warnings pass (extended with a MatchedPath-walking
variant for
sql_exprper §11.6).
- Schema-existence ERROR pass (extended for multi-binding
- Author the five new
diagnostic.*catalog keys (§11.5):unknown_qualifier,ambiguous_column,projection_alias_misplaced,cte_arity_mismatch,compound_arity_mismatch. - Author the eight new
engine.*catalog keys for friendly- error translations (§11.5).
Scope (out — explicit)
- The post-walk projection-list fixup pass (§10.6) — that is 2e.
- Detection of engine-rejected cases (§11.4) — those stay engine-side.
Build steps
- Extend the schema-existence pass to iterate every
from_scopebinding and the activecte_bindings. - Add qualified-reference resolution: when a
name_or_callmatch has a'.' identifiertail, resolve the qualifier againstfrom_scopealiases first, then table names. - Add ambiguity detection: an unqualified column ref that resolves into multiple bindings.
- Add projection-alias-misplaced check: walk
sql_exprslots in WHERE/GROUP BY/HAVING; identifier matches against the current frame'sprojection_aliases→ ERROR. - Author the arity-check pass: at every
ScopedSubgrammarexit, if the frame is a CTE body, compare declared vs derived column counts; if the parent context is a compound-query chain, compare against the previous leg's arity. - Author the MatchedPath-walking predicate-warnings variant:
identify
LIKE/=/!=withNULL/ mismatched-type comparison predicate-tails by node-name labels; emit the corresponding catalog key with the offending operand's span. - Author the catalog entries in the i18n catalog file with engine-neutral wording.
- Author tests per §11.2 and §11.6.
Exit gate
Required green tests:
- One positive and one negative test per new ERROR catalog
key (the negative confirms the key does not fire on a
well-formed equivalent):
diagnostic.unknown_qualifier:SELECT t.c FROM u(wheretis not in scope) fires;SELECT u.c FROM udoesn't.diagnostic.ambiguous_column:SELECT id FROM a JOIN b ON a.id = b.idfires for the bareid;SELECT a.id FROM a JOIN b ON a.id = b.iddoesn't.diagnostic.projection_alias_misplaced:SELECT a + b AS x FROM t WHERE x > 0fires;… ORDER BY xdoesn't (alias is bound by ORDER BY time).diagnostic.cte_arity_mismatch: `WITH x(a, b) AS (SELECT- SELECT * FROM x` fires; matched arity doesn't.
diagnostic.compound_arity_mismatch:SELECT 1, 2 UNION SELECT 1fires; matched arity doesn't.
- The Phase-1 gap closure explicitly verified:
SELECT * FROM products WHERE price LIKE 5firesdiagnostic.like_numeric(the test that would have caught the Phase-1 gap). Sister tests:WHERE name = NULLfiresdiagnostic.eq_null;WHERE age = 'old'firesdiagnostic.type_mismatch. - Predicate warnings extend across every SQL slot: the
same
LIKE-on-numeric pattern fires inHAVING,ON, aCASE WHENclause, a projection item, and anORDER BYitem — one test per slot. - Engine.* keys: integration tests that drive the engine to the error condition verify the friendly-error layer surfaces the engine-neutral wording (one test per key, run-and-assert shape).
Other gates:
- All 2a–2c tests still green.
- All 1260 baseline tests still green.
- Existing DSL predicate warnings (the DSL
ExprAST variant) still fire correctly — verify by re-running the DSL test suite section that exercises them. cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does every new catalog key have both a positive and a negative test, or did any slip in with only one?
- Was the Phase-1 gap closure verified, or did the test slip past in "well, it should work now"?
- Are the engine.* messages truly engine-neutral, or does any one leak SQLite product/feature names? (ADR-0030 §7.)
- Did 2d cover every Phase-2 case in §11.2, or were any silently downgraded to "engine handles"?
Sub-phase 2e — Completion + post-walk fixup
Scope (in)
- Add qualified-prefix completion narrowing (§10.5): when the
matched path immediately preceding an
IdentSource::Columnsslot ends withIdent '.', the completer narrows candidates to the named binding's columns. - Implement the post-walk fixup pass for projection-list
identifiers (§10.6): collect projection
Identterminals during the walk; after walk completion, re-resolve each against the finalfrom_scope; rewrite the highlight class and validity diagnostic in the walker's accumulated output. - The pass runs as a final stage of the walk itself (§10.6 integration-point convention).
Scope (out — explicit)
- Completion behaviour beyond column-name narrowing (keyword completion / function names / table names) — those come for free from the walker's expected-set machinery and need only cross-cut verification, not new code.
Build steps
- Add a "prefix qualifier" hint to the completion API that
the walker passes when the cursor is immediately after
Ident '.'. - Extend
IdentSource::Columnscompletion to honour the qualifier hint when present. - Implement the projection-
Identrecording during the walk (append a small struct per projection identifier to a walker-local list). - Implement the fixup pass that runs after the main walk: re-resolve each recorded identifier; rewrite the highlight run; update the diagnostics vector.
- Tests per the exit gate.
Exit gate
Required green tests:
- Qualified-prefix completion: at the cursor in
SELECT t.|, candidates aret's columns; atSELECT u.|whereuis not in scope, candidates are empty. - Alias-prefix completion: at the cursor in
SELECT FROM a AS x JOIN b AS y ON x.|, candidates arex's (i.e.a's) columns. - Qualified-prefix narrowing for CTE: at
WITH x AS (SELECT a, b FROM t) SELECT x.|, candidates areaandb(the derived columns). - Qualified-prefix narrowing for CTE with
SELECT *: atWITH x AS (SELECT * FROM users) SELECT x.|, candidates areusers' columns (per §10.3 derivation). - Projection-before-FROM: typing
SELECT col1, col2produces a generic-identifier highlight oncol1/col2(nofrom_scopeto resolve against); typingFROM tafter produces a column highlight oncol1/col2if they exist int, or an unknown-identifier diagnostic if they don't — verified within one debounce/walk cycle. ORDER BYalias resolution: parsesSELECT a + b AS total FROM t ORDER BY total; the alias resolves without a fixup pass (single-pass walk handles it per §10.6).- Completion across every Phase-2 slot: Tab at
WHERE |,GROUP BY |,HAVING |,ON |, projection|, andORDER BY |all offer expected candidates from the union offrom_scopebindings.
Other gates:
- All 2a–2d tests still green.
- All 1260 baseline tests still green.
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does the projection-before-FROM verification actually exercise the full re-walk cycle, or does it just call the fixup pass directly? (The user-observable behaviour is the re-walk on debounce; that is what the test must exercise.)
- Does the qualified-prefix narrowing produce empty candidates when the qualifier doesn't resolve, or does it accidentally fall back to the global column set?
- Did 2e introduce any priority/ordering for outer-frame candidates? ADR-0032 §10.5 explicitly says ordering is completion-tier polish, not specified here — if 2e added priority, that is an autonomous decision and escalates.
Sub-phase 2f — Type resolution
Scope (in)
- Add
"column_metadata"to the rusqlite feature list inCargo.toml(alongside"bundled"). - Implement the worker-side type-resolution helper that runs
after
prepare, before iterating rows: for each result column, queryRawStatement::column_table_name(i)/column_origin_name(i); if both return Some, look up the playground type fromSchemaCache.columns_for_table; otherwise leave the slot asNone. - Call the helper from
do_run_selectbefore constructing theDataResult.
Scope (out — explicit)
- Any new walker / grammar work. This is a worker-side change only.
Build steps
- Update
Cargo.toml.cargo buildto confirm the feature resolves throughlibsqlite3-sysand the bundled SQLite is rebuilt withSQLITE_ENABLE_COLUMN_METADATA. - Add the resolver method on
Database(probablyresolve_select_column_types(&Statement, &SchemaCache) -> Vec<Option<Type>>). - Wire into
do_run_select. - Tests per the exit gate.
Exit gate
Required green tests:
- One test per playground type (all ten:
text,int,real,decimal,bool,date,datetime,blob,serial,shortid) — a bare-column SELECT of a column of that type producescolumn_types[0] = Some(<that type>)and the renderer formats accordingly. boolrenders astrue/false(the pedagogically- important case lifting Phase-1 §4.5).- Computed expression stays typeless:
SELECT a + 1 FROM tproducescolumn_types[0] = None; the renderer uses neutral alignment. - Multi-column mixed test:
SELECT name, age, is_active, age + 1 FROM usersproduces[Some(text), Some(int), Some(bool), None]. - CTE pass-through:
WITH x AS (SELECT name FROM users) SELECT name FROM xrecoversSome(text)— this verifies the engine's column-origin metadata follows through the CTE in the bundled SQLite build (verified, not assumed). - Subquery result:
SELECT (SELECT name FROM users WHERE id = 1)— the engine's column-origin metadata may or may not follow through a scalar subquery; whichever it does, the test asserts the actual behaviour (not the wished-for behaviour) and an explanatory comment captures the engine's posture. - Unknown-table SELECT:
SELECT * FROM nonexistent— the resolver does not panic; the parse-time ERROR already fired in 2d.
Other gates:
- All 2a–2e tests still green.
- All 1260 baseline tests still green.
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
- Does the resolver handle every result-column case (named column, expression, NULL literal, CTE pass-through) without panicking?
- Is the rusqlite feature flag in place?
grep column_metadata Cargo.toml. - Did 2f surface any cases where engine-side metadata doesn't follow through (CTEs, views, subqueries)? Each such finding must be documented as an honest limitation, with the test capturing the actual behaviour.
Sub-phase 2g — Integration sweep + cross-cut verification
Scope (in)
The catch-all sub-phase. Verifies the full end-to-end Phase 2 surface and every cross-cut "X comes for free" claim. Produces the final phase-exit verification report (next section).
Build steps
- Author the end-to-end integration tests (Tier 3) for real-world-shape queries (the exit-gate list below).
- Author or extend the cross-cut verification matrix (next section) for every "X comes for free" claim from ADR-0030/0031/0032.
- Run the full test suite. Confirm 1260 baseline + all sub-phase 2a–2f additions + the 2g integration tests.
- Run
cargo clippy --all-targets -- -D warnings. - Produce the final verification report.
Exit gate
Required end-to-end integration tests:
- JOIN with WHERE:
SELECT a.name, b.total FROM customers a JOIN orders b ON a.id = b.customer_id WHERE a.country = 'DE'— runs, returns expected rows, the data-table renderer emits a clean output. - Recursive CTE: a small tree traversal (e.g., generate
integers 1..10 via
WITH RECURSIVE) — runs, returns 10 rows, renderer clean. - UNION of two SELECTs: runs, returns the union, renderer clean.
- Correlated EXISTS subquery:
SELECT name FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id)— runs, returns expected rows, renderer clean. history.logreplay: every Phase-2 statement form written above replays from the log faithfully (ADR-0030 §11 unchanged).reject_internal_table:FROM __rdbms_columnsparse- rejects;WITH __rdbms_x AS (…)parse-rejects; aFROM __rdbms_*inside a CTE body parse-rejects.
Cross-cut verification matrix (next section): every claim must have a passing test.
Other gates:
- Total test count: ≥ 1310 + (additions from 2b–2g). The exact count depends on per-step authorship; the gate is "every required-test row above and in the cross-cut matrix has at least one green test", not a magic number.
- Zero failures, zero skipped (excluding the unchanged doctest).
cargo clippy --all-targets -- -D warningsclean.
DA gate (written)
The final DA review, per CLAUDE.md Phase-5 verification:
- Does every requirement on the ADR-0032 checklist have a test? Walk the next-section matrix row by row.
- Are there any "free for free" claims from ADR-0030/0031/0032 that have no explicit test? (The Phase-1 gap pattern.) Each must have one.
- Did any sub-phase silently downgrade a requirement to "later"? Phase-1 §4.1–§4.5 were autonomous decisions flagged at handoff; any Phase-2 autonomous decision must also be flagged with the same discipline.
- Are ALL tests green AND zero-skipped? If not, the verdict is FAIL and 2g loops back.
Cross-cut verification matrix
Each row is a claim from ADR-0030, ADR-0031, or ADR-0032 that needs an explicit test, with the test's location. The matrix is filled in during 2g; the gate is "every row green".
| Claim source | Claim | Test location | Status |
|---|---|---|---|
| ADR-0030 §8 | Syntax highlighting works for SQL keywords | src/dsl/walker/highlight.rs::sql_select_keywords_classified, sql_expression_operators_classified_as_keywords |
✅ |
| ADR-0030 §8 | Tab completion works for SQL keywords | src/completion.rs::empty_input_offers_all_command_entry_keywords + entry-word probe via completion_probe_in_mode (Advanced) returns select/with |
✅ |
| ADR-0030 §8 | Hint-panel prose appears at every SQL grammar slot | src/dsl/walker/mod.rs::hint_mode_value_literal_slot_in_where_clause, typed_hint_at_where_value_uses_column_type |
✅ (representative slots; exhaustive coverage is a Phase-3 concern) |
| ADR-0030 §8 | [ERR]/[WRN] validity indicator fires for SQL |
src/dsl/walker/mod.rs::input_verdict_eq_null_is_warning, input_verdict_type_mismatch_is_warning |
✅ |
| ADR-0030 §8 | Per-command parse-error usage fires for SQL | tests/engine_vocabulary_audit.rs, tests/walking_skeleton.rs::typing_invalid_simple_input_shows_a_parse_error_not_an_echo |
✅ |
| ADR-0030 §9 | Out-of-subset construct (OVER (…)) produces engine-neutral parse error |
src/dsl/grammar/sql_select.rs::window_function_rejected |
✅ |
| ADR-0030 §11 | Every Phase-2 SQL statement form logs to history.log and replays |
tests/sql_select.rs::database_run_select_appends_to_history_when_source_present, tests/replay_command.rs |
✅ |
| ADR-0031 §5 | Highlighting works for SQL expression operators / CASE / function calls |
src/dsl/walker/highlight.rs::sql_expression_operators_classified_as_keywords, sql_case_expression_keywords_classified |
✅ |
| ADR-0031 §5 | Column completion works for IdentSource::Columns slots in sql_expr |
src/completion.rs::sql_expr_column_completion_inside_where, sql_expr_column_completion_inside_having |
✅ |
| ADR-0031 §5 | Hint prose appears at every sql_expr slot |
src/dsl/walker/mod.rs::typed_hint_at_where_value_uses_column_type (representative) |
✅ (representative; ADR-0024 HintMode plumbing means coverage flows from the grammar definition) |
| ADR-0032 §10.2 | DSL Subgrammar recursion (ADR-0026) does NOT push scope |
src/dsl/walker/driver.rs::subgrammar_walks_a_recursive_grammar (no frame side-effect) |
✅ |
| ADR-0032 §10.2 | sql_expr ladder Subgrammar recursion does NOT push scope |
src/dsl/grammar/sql_expr.rs::subquery_recursion_through_compound + the ladder uses Subgrammar not ScopedSubgrammar for internal recursion |
✅ |
| ADR-0032 §10.2 | ScopedSubgrammar (SELECT) DOES push scope |
src/dsl/walker/driver.rs::scoped_subgrammar_baseline_frame_is_always_present, scoped_subgrammar_walks_a_recursive_grammar |
✅ |
| ADR-0032 §10.3 | SELECT * CTE body derives output columns from from_scope |
src/dsl/walker/driver.rs::cte_harvest_star_expands_from_scope |
✅ |
| ADR-0032 §10.3 | Computed CTE projection without alias yields unnamed slot | src/dsl/walker/driver.rs::cte_harvest_computed_no_alias_is_unnamed |
✅ |
| ADR-0032 §10.3 | CTE (col-list) renames positionally |
src/dsl/walker/driver.rs::cte_harvest_col_list_renames_positionally |
✅ |
| ADR-0032 §10.3 | Compound CTE body takes columns from first leg | src/dsl/walker/driver.rs::cte_harvest_compound_takes_first_leg |
✅ |
| ADR-0032 §10.3 | Recursive CTE body takes columns from non-recursive leg | src/dsl/walker/driver.rs::cte_harvest_recursive_uses_non_recursive_leg |
✅ |
| ADR-0032 §10.6 | Projection-before-FROM re-resolves on full re-walk | src/dsl/walker/mod.rs::projection_before_from_tests module (4 tests) |
✅ (via 2d two-pass diagnostic + diagnostic-overlay renderer; see ADR-0032 Amendment 2) |
| ADR-0032 §11.6 | Phase-1 gap: LIKE-on-numeric fires on SQL WHERE |
src/dsl/walker/mod.rs::sql_where_like_numeric_warns |
✅ |
| ADR-0032 §11.6 | = NULL fires on SQL WHERE |
src/dsl/walker/mod.rs::sql_where_eq_null_warns |
✅ |
| ADR-0032 §11.6 | Type-mismatch fires on SQL WHERE |
src/dsl/walker/mod.rs::sql_where_type_mismatch_text_vs_number_warns, sql_where_type_mismatch_number_vs_text_warns |
✅ |
| ADR-0032 §11.6 | Predicate warnings fire on HAVING, ON, CASE, projection, ORDER BY |
src/dsl/walker/mod.rs::sql_having_predicate_warning_fires, sql_join_on_predicate_warning_fires, sql_case_predicate_warning_fires, sql_order_by_predicate_warning_fires, sql_projection_predicate_warning_fires |
✅ |
| ADR-0032 §11.4 / §13 | Aggregate-in-WHERE rejected by engine, surfaced engine-neutral | src/friendly/translate.rs::aggregate_misuse_engine_message_routes_through_catalog |
✅ |
| ADR-0032 §11.4 / §13 | Non-aggregated-column-with-GROUP-BY engine-neutral | src/friendly/translate.rs::group_by_required_engine_message_routes_through_catalog |
✅ |
| ADR-0032 §9 | Depth cap fires on pathological SELECT nesting (≥ 64 frames) | src/dsl/grammar/sql_select.rs::pathological_nesting_capped, src/dsl/walker/driver.rs::subgrammar_depth_cap_rejects_pathological_nesting |
✅ |
| ADR-0032 §12 | Engine column-origin metadata follows through CTE | tests/sql_select.rs::database_run_select_recovers_bool_column_type, database_run_select_recovers_text_type_through_alias |
✅ |
| ADR-0032 §12 | All 10 playground types recovered via bare ref | tests/sql_select.rs::database_run_select_recovers_all_ten_playground_types |
✅ |
| ADR-0032 §13 | Every OOS shape rejects (NATURAL, USING, comma-FROM, comma-LIMIT, window, LATERAL, VALUES) | src/dsl/grammar/sql_select.rs::comma_from_is_rejected, natural_join_rejected, using_clause_rejected, values_row_source_rejected, lateral_join_rejected, window_function_rejected |
✅ |
The implementer fills in Test location and Status (green
checkmark or red cross) as 2g proceeds. A row marked red blocks
the phase exit.
Final phase-exit verification report
Produced at the end of 2g. A markdown document at
docs/handoff/<date>-phase-2-verification.md (or similar). The
report contains:
- Test suite totals — passed / failed / skipped / ignored,
exact numbers from
cargo testoutput. Compared against the Phase-1 baseline (1260 / 0 / 0 / 1). - The full cross-cut verification matrix with every row green.
- A requirements-to-test mapping — every numbered decision in ADR-0032 §§1–13 with the test(s) that prove it.
- A list of any autonomous decisions made during implementation, with rationale and explicit user-confirmation pointer for each. (Per CLAUDE.md: an autonomous decision without explicit user confirmation is a verdict FAIL.)
- The DA's written final review — PASS or FAIL, no "conditional" verdicts.
Only after the report is produced and signed off does the user-facing "Phase 2 complete" message issue.
Open questions to escalate before code starts
These are matters the plan deliberately leaves to the implementer to escalate when reached, not to decide silently:
- Subquery type-resolution through scalar subqueries. The 2f exit gate notes the engine's column-origin metadata may or may not follow through a scalar subquery. The test asserts the actual behaviour; if the behaviour is "doesn't follow", that becomes an honest limitation in §12 — the user is informed, the limitation is documented.
- Duplicate CTE-name detection. §11.4 leaves this as
engine-rejected (no pre-flight diagnostic). If 2g's
verification finds the engine message is confusing, escalate:
the user may want a pre-flight
diagnostic.duplicate_cteERROR, which would mean a small extension to 2d. - Function name completion / allowlist. §11.4 / ADR-0030 §13 OOS-3 leaves no allowlist. If real-world testing reveals learners are confused by silently-admitted-then-engine-rejected function names, escalate for an explicit ADR.
What this plan does NOT contain
- Time estimates. Phase 2 is large and ADR-0032 is the first pass through the problem; calibration is poor. Milestones, not hours.
- Sub-phase commit granularity beyond "ship green tests at each gate". The implementer commits at natural break points within a sub-phase; the gate is what gets verified.
- Re-statements of ADR-0032's decisions. The plan refers to ADR-0032 by section; the implementer reads both.