217 lines
12 KiB
Markdown
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.
|