Files

12 KiB

Session handoff — 2026-05-27 (44)

Forty-fourth handover. This session completed ADR-0036 — value handling for advanced-mode SQL DML — by shipping Phase 2, Phase 3a, and Phase 3b (picking up from handoff-43, which had landed Phase 1), then closing the one Phase-1 carryover gap. ADR-0036 is now fully implemented; the next session starts from a clean slate on it.

§1. State at handoff

Branch: main. HEAD d987171. Tests: 1948 passing, 0 failing, 0 skipped, 1 ignored (the friendly/mod.rs ```ignore doctest). Clippy: clean (cargo clippy --all-targets -- -D warnings).

This session's commits (newest first):

d987171 fix: ADR-0036 — name the offending value for natural-order SQL INSERTs
8906661 feat: ADR-0036 Phase 3b — live typed-slot hints + highlighting for INSERT VALUES
49ea03b feat: ADR-0036 Phase 3a — live typed-slot hints + highlighting for SQL SET values
8c3b13b feat: ADR-0036 Phase 2 — validate advanced-mode UPDATE SET literals + retain the value

§2. What shipped — ADR-0036 complete

Read docs/adr/0036-typed-dml-values-vs-verbatim.md in full, and especially Amendment 1, which is now the authoritative record of the Phase 3 mechanism (it supersedes the original §5 sketch — see §3 below).

The four ADR-0036 phases, end to end:

  • Phase 1 (handoff-43) — INSERT … VALUES literal validation. Capture each literal value position at parse onto Command::SqlInsert.literal_rows; do_sql_insert validates them against column types (shared impl_value_for) before the verbatim insert; the error enricher names the offending value. No grammar change.
  • Phase 2 (8c3b13b) — UPDATE … SET literal validation. The same capture-at-parse technique on the SET assignment list: capture_set_literals (src/dsl/grammar/data.rs) classifies each top-level SET col = <rhs> into (col, Some(Value)) (a bare literal, incl. a signed number) or (col, None) (an expression), using paren depth so a comma in a function call or a where in a subquery isn't a boundary, and excluding the trailing top-level WHERE. Command::SqlUpdate gained set_literals; do_sql_update validates; user_value_for_column reads them. WHERE is deliberately not validated.
  • Phase 3a (49ea03b) — live typed-slot hints + highlighting for SET col = <literal>. The SET column ident now sets writes_column: true (so current_column / pending_value_column are populated per assignment); the RHS became the shared shared::SET_VALUE slot — a boundary-aware lookahead that routes a lone literal to the column-typed slot (live hint + numeric-shape highlight, shared with the DSL) and any expression to sql_expr. Covers sql_update's assignment list and sql_insert's ON CONFLICT … DO UPDATE SET.
  • Phase 3b (8906661) — live per-position typed slots for INSERT … VALUES (…) (single/multi-row, Form A and Form B). The hard half — see §3.

Phase-1 carryover closed (d987171)

A no-column-list (natural-order / Form B) SQL INSERT that hit a UNIQUE/CHECK violation used to degrade to the neutral "that value" because user_value_for_column only resolved the offending value for the explicit-column-list form. user_value_for_column_with_schema (src/runtime.rs) now maps each VALUES position to the schema's columns in declaration order — all columns (advanced-mode Form B auto-fills nothing, so every column has a value), single-row only (multi-row stays ambiguous → neutral). This was the only tracked ADR-0036 follow-up; it is done.

§3. Phase 3 design — the load-bearing details

Two design forks were escalated to and decided by the user; both are recorded in ADR-0036 Amendment 1. Future work on the grammar must respect them.

The mechanism is a boundary-aware lookahead, NOT a naive Choice

ADR-0036 §5 originally sketched Phase 3 as Choice(typed-literal-slot, sql_expr) at each value position. That is wrong and would regress valid SQL. The walker's Node::Choice is first-match-wins with no cross-branch backtrack, so a typed slot would greedily match the leading 1 of 1 + 2 and commit, leaving + 2 dangling — breaking, e.g., the existing values (1, 1 + 2) test. The correction (Amendment 1): a lookahead peeks the whole value position and routes a literal to the typed slot only when the literal fills the position (up to the next , / ) / ; / where / returning / end); everything else goes to sql_expr. shared::SET_VALUE (set_value_node + set_rhs_is_lone_literal) is that primitive; it's reused by both 3a and 3b. Empty positions route to the typed slot too, so the hint shows from the moment the cursor lands.

Phase 3b's new Node::SetColumn primitive + arity reconciliation

INSERT … VALUES positions are positional (no per-position column ident) and multi-row, so the user approved adding a grammar primitive (the "full parity" option):

  • Node::SetColumn(&'static TableColumn) (src/dsl/grammar/mod.rs, walker arm in src/dsl/walker/driver.rs) — a zero-width node that sets current_column + pending_value_column, like an Ident { writes_column: true } but without consuming input. One enum variant, one walker arm (only one exhaustive Node match site exists).
  • The factory sql_insert::sql_value_list emits SetColumn(colᵢ) then SET_VALUE per position. Column mapping mirrors do_sql_insert: Form A → listed columns in user order; Form B → ALL columns in declaration order.
  • Auto-fill clarification (corrects a loose statement made while scoping): advanced mode does auto-fill an omitted shortid in Form A (plan_shortid_autofill) — that column has no VALUES position and is correctly absent from the mapping; it does not auto-fill serial (the X4 gap); and Form B auto-fills nothing (the function returns early on an empty column list).
  • Arity reconciliation (the subtle part). A fixed-length typed Seq would reject a wrong-arity tuple, suppressing the friendly per-tuple insert_arity_mismatch diagnostic (ADR-0033 §8.1), which is a post-walk pass over the matched path and so needs the tuple to be accepted. So the tuple value list is an arity-gating lookahead (tuple_value_list + count_tuple_values): a closed tuple uses the typed Seq only on an exact value/column match; an open (mid-typing) tuple uses it while count <= columns (so the hint shows from ( onward); every other case falls back to the type-blind Repeated(sql_expr), leaving §8.1 to fire unchanged. Correct-arity tuples (the common case) get full live typed feedback — including a wrong-kind lone literal like ('text') into an int column — while wrong-arity tuples keep the friendly arity message.

Known limitation (all phases, matches the DSL): date / shortid / datetime format is still not validated at parse — those slots accept any quoted string; format is checked at bind/execution time. The live highlight catches numeric-shape mismatches (int/decimal/bool); the column-type hint shows for every type.

§4. Where the code lives (ADR-0036 surface)

  • Parse-time capture / typed slots: src/dsl/grammar/data.rs (capture_literal_rows, capture_set_literals, build_sql_insert, build_sql_update); src/dsl/grammar/shared.rs (SET_VALUE, set_value_node, set_rhs_is_lone_literal, next_is_set_boundary, the typed *_SLOTs, current_column_value); src/dsl/grammar/sql_insert.rs (sql_value_list, tuple_value_list, count_tuple_values, target_value_columns, fallback_value_list, VALUE_TUPLE); src/dsl/grammar/sql_update.rs (ASSIGN_COLUMN, SET_VALUE RHS).
  • The primitive: Node::SetColumn in src/dsl/grammar/mod.rs; its arm in src/dsl/walker/driver.rs.
  • Worker validation: src/db.rs do_sql_insert / do_sql_update (validate the captured literals via impl_value_for).
  • Error enrichment: src/runtime.rs user_value_for_column / user_value_for_column_with_schema.
  • Tests: tests/sql_update.rs (Phase 2 + 3a, incl. the advanced-mode typing-surface helpers schema_cache + ambient_hint_in_mode / classify_input_with_schema_in_mode against Mode::Advanced); tests/sql_insert.rs (Phase 3b, vschema + prose_at); tests/friendly_enrichment.rs (offending-value enrichment for SqlUpdate + natural-order SqlInsert).

§5. Open / tracked work (none from ADR-0036)

  • X4 — serial non-PK auto-fill difference (possible bug, the most concrete next item). requirements.md Cross-cutting. Simple-mode do_insert auto-fills an omitted non-PK serial with MAX(col)+1; advanced-mode (plan_shortid_autofill) auto-fills only shortid, never non-PK serial. Likely unintended. Decide whether advanced mode should match (probably yes) and align ADR-0018. Untouched by ADR-0036 (surgical by design).
  • X5 — framework cohesion / restructuring (strategic). requirements.md Cross-cutting. The grammar/execution framework grew organically; a descriptive ADR (map what exists + the share-a-mechanic-not-a-command reuse-boundary rule) is the likely cheaper first step before any restructure. ADR-0036 is a worked example of the rule in practice. Not scheduled.
  • Long-tail (carried from handoff-39 §8 / handoff-43 §5): app-lifecycle runtime-failure journalling; M4 execution-time mode side-channel; the blob value literal (belongs in the literal layer); CI/TT5 (test infra exists, no workflow yet); the DSL→SQL teaching echo (ADR-0030 Phase 5); H1 full SQL→English error translation; H1a syntax-help in parse errors; tutorial/lesson system (needs its own ADR); V3/V4 (ER diagram export, session log + Markdown export); the I- series input UX (readline shortcuts, multi-line, tab completion, syntax highlighting).

§6. Process pins (reinforced this session)

  • Confirm every commit (propose the message, wait). No AI attribution.
  • Escalate grammar-touching / architectural forks; don't decide for the user. This session escalated two Phase-3 forks (the lookahead mechanism correction; the 3a/3b split and the 3b full-parity-vs- pragmatic choice) — both materially changed the work and were the user's call. The arity-diagnostic conflict surfaced during 3b implementation (an exploration gap) and was reconciled within the approved option without regressing §8.1 — flagged to the user rather than absorbed silently.
  • /runda-style verification at planning exit earns its keep, and so does re-checking assumptions against the code: the "advanced mode doesn't auto-fill serial/shortid" claim was wrong for shortid, caught by the user and confirmed against plan_shortid_autofill.
  • Keep docs lockstep — ADR body + Amendment + README index move with the code in the same change.
  • ADRs aren't re-litigated casually — when a decision needs to change (the Phase 3 mechanism), write an Amendment. Amendment 1 is the live Phase 3 record.

§7. How to take over

  1. Read, in order: this file → CLAUDE.mddocs/adr/0036-typed-dml-values-vs-verbatim.md (esp. Amendment 1) → docs/requirements.md X4/X5.
  2. Baseline: cargo test (1948 / 0 / 0 / 1 ignored) + cargo clippy --all-targets -- -D warnings (clean).
  3. Pick up X4 (the most concrete tracked bug) or a long-tail item with the user. For X4, the relevant code is db.rs do_insert (simple-mode MAX+1 serial fill) vs plan_shortid_autofill (advanced-mode shortid-only fill), and ADR-0018 is the decision to align.