Files

217 lines
12 KiB
Markdown

# 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 `*_SLOT`s, `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.md`
`docs/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.