# Session handoff — 2026-05-20 (26)

Twenty-sixth handover. **Design session.** ADR-0032 (the full
SQL `SELECT` grammar — ADR-0030 Phase 2) is **Accepted** and an
**implementation plan** sits next to it under a new
`docs/plans/` directory. No code changed this session; the next
session is implementation territory.

## §1. State at handoff

**Branch:** `main`. Working tree clean. One unpushed commit
(`be8a4f5..a7db7dd`).

**Tests:** **1260 passing, 0 failing, 1 ignored** — unchanged
from handoff-25 (no code touched).

**Clippy:** clean (last verified at handoff-25; no code changed
since).

**New commit since handoff-25:**

```
a7db7dd docs: ADR-0032 + Phase 2 plan — full SQL SELECT grammar
```

A single docs commit; ADR + plan + index update ship as a
coherent unit because the plan is Phase-2's mechanics for
ADR-0032's decisions.

## §2. What got designed — the shape now

Read in order:

1. **`docs/adr/0032-sql-select-grammar.md`** — the grammar
   decisions for Phase 2.
2. **`docs/plans/20260520-adr-0032-phase-2.md`** — the
   implementation plan walking the work in seven sub-phases.

### 2.1. ADR-0032 in one screen

Phase 2's grammar surface (§§1–9 of the ADR):

- Five JOIN flavours: `INNER`, `LEFT [OUTER]`, `RIGHT [OUTER]`,
  `FULL [OUTER]`, `CROSS`. NATURAL/USING/comma-FROM are OOS.
- All four set ops: `UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`.
- CTEs: `WITH` and `WITH RECURSIVE`, with optional `(col-list)`.
- Subquery expressions as additive `sql_expr` branches: scalar
  `(SELECT …)` primary, `IN (SELECT …)`, `[NOT] EXISTS (…)`.
  Redeems ADR-0031 §7 OOS-1.
- Qualified column refs `t.c` / `alias.c` as a `name_or_call`
  tail. Redeems ADR-0031 §7 OOS-2.
- `LIMIT n [OFFSET m]`; legacy comma form OOS.
- `DISTINCT` / `ALL`, `t.*` projection, bare-alias projection
  (lifts Phase-1 §4.2's autonomous decision).
- One new grammar file: `src/dsl/grammar/sql_select.rs`,
  parallel to `sql_expr.rs`, exporting `SQL_SELECT_STATEMENT`
  and `SQL_SELECT_COMPOUND`.

Walker-capability honesty (§10) — the part where ADR-0030 §8's
"ambient assistance comes for free" got softened:

- **Grammar recursion** still needs nothing new — `Subgrammar`
  + ADR-0026's depth cap unchanged.
- **Completion scope** needs a new node variant
  `Node::ScopedSubgrammar(&Node)` alongside the existing
  `Node::Subgrammar`, plus a `from_scope_stack: Vec<ScopeFrame>`
  on `WalkContext`. Each `ScopeFrame` holds `from_scope`,
  `cte_bindings`, `projection_aliases`. Existing DSL `Expr`
  and `sql_expr` recursion stays on the non-scope-pushing
  variant and is unaffected.
- **CTE column resolution** (§10.3): `SELECT *` and explicit-
  projection CTE bodies both yield real column completion past
  `cte_alias.|` via a body-projection derivation rule that
  runs at the body's `ScopedSubgrammar` exit. The earlier
  "honest limitation" posture is replaced.
- **Qualified-prefix completion** (§10.5): at `t.|`, candidates
  are scoped to `t`'s binding.
- **Projection-before-FROM problem** (§10.6): the debounced
  re-walk catches up via a post-walk fixup pass that re-
  resolves projection-list identifiers against the final
  `from_scope`, rewriting highlight class and validity
  diagnostic in the walker's accumulated output. Runs as a
  final stage of the walk itself — same convention applies to
  the §11.6 predicate-warnings pass.

Diagnostics (§11) — every Phase-2 validation case classified
against ADR-0027's ERROR/WARNING guideline:

- **Five new `diagnostic.*` keys** (parse-time-detectable):
  `unknown_qualifier`, `ambiguous_column`,
  `projection_alias_misplaced`, `cte_arity_mismatch`,
  `compound_arity_mismatch`.
- **Eight new `engine.*` keys** for friendly-error layer
  translations of engine messages.
- **Three diagnostic passes by end of Phase 2** (§11.7):
  schema-existence (extended for multi-binding scope), arity-
  check (new), predicate-warnings (extended with a
  MatchedPath-walking variant — §11.6).

Result-column type resolution (§12):

- `rusqlite` 0.39.0 exposes `column_table_name` /
  `column_origin_name` / `column_database_name` on
  `RawStatement` behind a `column_metadata` feature.
  **Verified** during this session by reading the rusqlite
  source. `Cargo.toml` change is one line — add
  `"column_metadata"` to the feature list.
- Bare column refs recover their playground type via that
  metadata; partially lifts Phase-1 §4.5's bool→0/1 deferral.

### 2.2. The implementation plan

`docs/plans/20260520-adr-0032-phase-2.md` (881 lines) breaks
Phase 2 into seven sub-phases:

- **2a** — Grammar fragment (`sql_select.rs`, no walker
  changes yet).
- **2b** — `Node::ScopedSubgrammar` variant + the scope
  accumulators on `WalkContext`.
- **2c** — Phase-1 grammar migration: move Phase-1 SELECT
  nodes from `data.rs` into `sql_select.rs`; the 7 Phase-1
  integration tests are the safety net.
- **2d** — Diagnostics passes + catalog keys.
- **2e** — Completion + post-walk fixup pass.
- **2f** — Type resolution via rusqlite `column_metadata`.
- **2g** — Integration sweep + the cross-cut verification
  matrix (the Phase-1-gap-prevention mechanism).

Each sub-phase has explicit exit gates with required tests and
a DA-prompt checklist for the gate review. The plan also
encodes the user's standing authorizations:

- **Walk uninterrupted between gates** unless there's a real
  ambiguity, autonomous decision, blocker, or in-the-plan-open-
  question trigger.
- **Commits pre-authorized** at gate boundaries and natural
  break points within a sub-phase. Standard `area: subject`
  message style, detailed body, no AI attribution.
- **Pushes remain user-only.**
- **End-of-plan hand-back** is the one explicit pause: the
  verification report + DA PASS verdict get surfaced to the
  user before Phase 2 is declared complete.

The cross-cut verification matrix (29 rows) names every "X
comes for free" claim from ADR-0030/0031/0032 with a test-
location placeholder. The Phase-1 SQL-expression predicate-
warning gap is row 20 by name — making sure an analogous
silent gap cannot ship undetected.

## §3. The Phase-1 finding worth carrying forward

While walking ADR-0027 Amendment 1's existing-cases inventory
during this session, the warning-vs-error guideline check
surfaced a **Phase-1 carry-over gap**:

ADR-0027 Amendment 1's `LIKE`-on-numeric warning — together
with ADR-0026 §7's `= NULL` and type-mismatch warnings — is
emitted by a pass that walks the **DSL `Expr` AST**. Phase 1's
`sql_expr.rs` deliberately builds **no AST** (ADR-0031 §2).
The consequence: **SQL `WHERE` expressions today emit none of
these warnings.** `select * from t where name like 5` parses,
the engine runs it, and the learner gets the engine's verdict,
not the friendly pre-flight nudge Amendment 1 promised.

Phase 1 shipped structurally green (1260 / 0 / 1) but the
validity indicator was effectively dormant on SQL expressions —
the test suite never asked "does this warning fire on a SQL
WHERE input?" because there was no test for the cross-cut "the
free-for-free promise still holds for SQL" claim.

ADR-0032 §11.6 closes the gap (Phase 2's diagnostics work
implements a MatchedPath-walking predicate-warnings variant
covering every `sql_expr` slot — WHERE, HAVING, ON, CASE,
projection, ORDER BY) and the implementation plan's cross-cut
matrix has it as a named row to prevent a regression. The plan
is structured so this kind of gap cannot ship undetected again.

**Worth carrying forward to future phase planning:** every
"comes for free" claim in any ADR is also a candidate for an
unnoticed silent gap. The cross-cut matrix pattern in the plan
generalises.

## §4. Autonomous decisions during this session

Per CLAUDE.md, decisions outside ADR-0030's settled scope are
flagged here. None are urgent; all were either confirmed by the
user via explicit AskUserQuestion gates or are routine detail.

- **Catalog key naming.** `diagnostic.*` for parse-time
  diagnostics and `engine.*` for friendly-error translations
  — chosen by following the existing pattern
  (`diagnostic.unknown_table`, `select.internal_table`); no
  new naming convention introduced.
- **Subgrammar push-trigger mechanism** (§10.2) — `Node::Scoped­
  Subgrammar(&Node)` chosen via explicit user
  AskUserQuestion gate over two alternatives (flag on existing
  variant; driver-side identity registry). Recommended option;
  user picked Recommended.
- **CTE body column derivation** (§10.3) — six derivation rules
  covering `*`, `t.*`, bare refs, qualified refs, aliased
  expressions, and computed-no-alias; first leg / non-recursive
  leg dictates for compound / recursive bodies. Per standard
  SQL throughout.
- **`Node::ScopedSubgrammar` data model** — `ScopeFrame` is a
  struct holding `from_scope` / `cte_bindings` /
  `projection_aliases` together rather than three parallel
  stacks. Cleaner data model; no semantic difference. Settled
  during writing.

Every scope question that affected expressiveness or surface
area went through an `AskUserQuestion` gate. The full list of
those gates appears in the conversation log; the answers shape
the ADR's §§1–10 directly.

## §5. What's next — Phase 2 implementation

Per ADR-0030's phasing, Phase 2 is the next slice. The user
has signed off on the plan and authorized walking it without
interruption between gates. The implementer for Phase 2 should:

1. **Read** in order: this handoff, ADR-0032, the Phase 2
   plan, then `CLAUDE.md` (working-style rules unchanged) and
   `docs/requirements.md` (`Q1`/`Q2` advance further; `Q4`
   already ticked).
2. **Begin sub-phase 2a** — the grammar fragment. The plan's
   "Scope (in)" / "Build steps" / "Exit gate" / "DA gate" sections
   for 2a are the spec.
3. **Commit at gate boundaries** with detailed messages in the
   `area: subject` style (recent precedent: `grammar: SQL
   SELECT end-to-end (ADR-0030 Phase 1)`, etc.).
4. **Escalate when the plan says to** — see §2 of the plan's
   "Standing authorizations" for the four trigger conditions.
   The plan's "Open questions to escalate before code starts"
   section pre-identifies three known triggers (subquery type-
   resolution through scalar subqueries; duplicate CTE-name
   detection; function name allowlist).
5. **At end of 2g**, produce the verification report and
   surface to the user; do NOT declare Phase 2 complete
   without the user's confirmation. This is the one explicit
   pause-point per §5 of the plan's standing authorizations.

After Phase 2 ships and is accepted, ADR-0030 Phase 3 (DML —
INSERT / UPDATE / DELETE in SQL) is the next focus area.

## §6. Seams for the implementer

These are the file-and-section pointers for Phase 2 work,
copied across from the plan for fast access:

- **`src/dsl/grammar/sql_select.rs`** (new) — the §1 grammar.
  Parallel to `sql_expr.rs` (ADR-0031). Exports
  `SQL_SELECT_STATEMENT` (the full statement with optional
  `WITH`) and `SQL_SELECT_COMPOUND` (the embedded form
  subqueries recurse into).
- **`src/dsl/grammar/sql_expr.rs`** — three additive `Choice`
  branches (scalar subquery `primary`, `IN (subquery)`, `[NOT]
  EXISTS (subquery)`) and one additive `name_or_call` tail
  (qualified ref `t.c`). All recurse through
  `Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND)`.
- **`src/dsl/grammar/mod.rs`** — `Node` enum gets the new
  `ScopedSubgrammar(&'static Node)` variant alongside the
  existing `Subgrammar`.
- **`src/dsl/walker/context.rs`** — `WalkContext` gains
  `from_scope_stack: Vec<ScopeFrame>`. `current_table` /
  `current_table_columns` become derived helpers over the top
  frame (DSL paths must stay green — the existing 1240+ test
  surface verifies this).
- **`src/dsl/walker/driver.rs`** (or wherever the Subgrammar
  match arm lives) — new arm for `Node::ScopedSubgrammar` that
  pushes/pops a `ScopeFrame`.
- **`src/dsl/walker/mod.rs`** — `schema_existence_diagnostics`
  extended to walk every `from_scope` binding; new
  arity-check pass; existing `predicate_warnings` gets a
  MatchedPath-walking variant for `sql_expr` slots.
- **`src/db.rs`** — `do_run_select` calls a new
  `resolve_select_column_types` helper before constructing the
  `DataResult`. The helper uses
  `RawStatement::column_table_name(i)` /
  `column_origin_name(i)` per result column.
- **`Cargo.toml`** — add `"column_metadata"` to rusqlite's
  feature list. One-line change.
- **`src/dsl/grammar/data.rs`** — Phase-1 SQL SELECT static
  nodes either move into `sql_select.rs` or get removed
  outright (the `data::SELECT` `CommandNode` is rebuilt
  against `SQL_SELECT_STATEMENT`); see plan sub-phase 2c.
- **Catalog file** (wherever the i18n keys live; the existing
  `diagnostic.unknown_table` neighbours) — five new
  `diagnostic.*` keys + eight new `engine.*` keys per ADR-0032
  §11.5.

## §7. How to take over

1. **Read this file, then `docs/adr/0032-sql-select-grammar.md`
   and `docs/plans/20260520-adr-0032-phase-2.md`.** Then
   `CLAUDE.md` and `docs/requirements.md`.
2. **`cargo test`** — expect 1260 passing, 0 failed, 1 ignored
   (the baseline).
3. **`cargo clippy --all-targets -- -D warnings`** — clean.
4. **Start sub-phase 2a** per the plan. Standing
   authorizations (plan §§1–5) apply.
5. **Escalate per CLAUDE.md when the plan's triggers fire;
   never decide silently** on a question the ADR/plan doesn't
   settle.

## §8. What else is open

Unchanged from handoff-25 §8 (prioritisation is a **user
decision**): snapshot/undo `U`-series (ADR-0006 written,
unbuilt); m:n convenience `C4`; modify-relationship `C3a`;
`show` family `V5`; rename-table `C1` (may fall out of ADR-0030
Phase 4); friendly-error sweep `H1`; CI `TT5`; session-log /
Markdown export `V4`.

ADR-0030's later phases — DML (Phase 3), DDL (Phase 4), the
DSL→SQL teaching echo (Phase 5), polish (Phase 6) — remain on
the roadmap behind Phase 2.

Phase-1's specific deferrals (§4.1–§4.5 in handoff-25) are
addressed as follows by ADR-0032:

- **§4.1 — `FROM` optional.** Stands. ADR-0032 §1 notes
  `SELECT 1` and `SELECT upper('x')` continue to parse.
- **§4.2 — implicit projection aliasing (`select a x`).**
  Lifted by ADR-0032 §1 — admitted in Phase 2.
- **§4.3 — `help_id: None` on SELECT.** Stands; Phase 6.
- **§4.4 — walker entries defaulting to internal `Mode::Simple`.**
  Stands; revisited when advanced mode grows its own ambient
  assistance (Phase 6).
- **§4.5 — bool SELECT results render as `0`/`1`.** Partially
  lifted by ADR-0032 §12 — bare bool column refs now recover
  their type via engine column-origin metadata. Computed
  bool expressions stay typeless until a future enhancement.
