Files
rdbms-playground/docs/handoff/20260522-handoff-31.md

265 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session handoff — 2026-05-22 (31)
Thirty-first handover. This session shipped, in order:
1. **Finished the advanced-mode completion interlude** left open by
handoff 30 — the **F5** core `walk_seq` fix, plus a follow-up
`walk_repeated` fix and a candidate-ordering reversal that grew
out of manual testing.
2. **Advanced ADR-0033 Phase 3 through sub-phases 3c, 3d, 3e**
SQL `INSERT … SELECT`, `shortid` auto-fill, and SQL `UPDATE`.
The next session should **start sub-phase 3f** (`DELETE` +
cascade summary) — the largest remaining DML sub-phase — which is
why we're handing off first. See §4 for a key 3f heads-up.
## §1. State at handoff
**Branch:** `main`. **Tests: 1524 passing, 0 failing, 1 ignored.**
**Clippy:** clean (`cargo clippy --all-targets -- -D warnings`).
**Commits this session** (newest first). `origin/main` is at
`6ff9144` (3c), so the **last three are local-only**:
```
53808ed grammar+db: 3e — SQL UPDATE grammar + execution (ADR-0033 §2) (local)
18d34d0 db: 3d fix — don't let shortid auto-fill mask INSERT arity mismatch (local)
78ad476 db+grammar: 3d — shortid auto-fill for SQL INSERT (ADR-0033 §6) (local)
6ff9144 grammar: 3c — INSERT … SELECT row source (ADR-0033 §4) (origin/main)
7f68a53 walker+completion: surface list trailing-optionals + identifiers-first ordering (ADR-0022 Amendment 2)
43c49f4 walker: F5 — drop preceding-clause keywords from committed-child Incomplete sets
```
(Unpushed commits are a normal working state; pushing is the
user's step — do not prompt about it.)
## §2. THE process lesson (read this first)
This session's clearest signal is about **Devil's Advocate
discipline**, not code. Three times the lead produced a DA
"verdict" that merely restated the exit gate — and three times the
*user* had to prompt before a genuine adversarial pass happened.
Each real pass then found something concrete:
- "What about more than one shortid in a row?" → an **untested**
multi-shortid path (it worked, but had zero coverage).
- "Did you check with the DA?" → the **arity-masking bug** (3d
auto-fill silently dropped extra columns / could read out of
range). Real correctness bug; fixed in `18d34d0`.
- "Sounds like the DA had nothing to add." → the **untested render
guard** in 3e (`handle_dsl_update_success`).
**Takeaway for the next agent:** do the adversarial work
*proactively* and *in writing*, and make it produce a failing test
or a concrete change — not a paragraph that clears everything.
"The exit-gate tests pass" is not a DA review. Attack the code:
untested branches, edge cases the happy-path tests skip, behaviour
that diverges from the non-SQL path, things that only break when an
input is malformed. A clean PASS with no findings on a non-trivial
sub-phase should itself be suspicious.
## §3. Phase 3 — what shipped (3c / 3d / 3e)
Controlling docs (read both):
- `docs/adr/0033-sql-dml-grammar.md` — the decision (read
**Amendment 1** for the dispatch mechanism).
- `docs/plans/20260520-adr-0033-phase-3.md` — build order
(3a3k), per-sub-phase exit + DA gates.
### 3c — `INSERT … SELECT` (`6ff9144`)
Grammar-only: the INSERT row source became a
`Choice(VALUES_CLAUSE, Subgrammar(&sql_select::SQL_SELECT_COMPOUND))`.
`SQL_SELECT_COMPOUND` is itself a `Choice` that admits a leading
`WITH`, so the R4 (WITH-prefixed) row source parses for free. No
worker change — `do_sql_insert` already executes the validated SQL
and the engine handles insert-from-query.
### 3d — `shortid` auto-fill (`78ad476`) + arity fix (`18d34d0`)
`Command::SqlInsert` gained `listed_columns` and `row_source`,
extracted in `build_sql_insert` from the matched path (the row
source is found by the **first `values`/`select`/`with` Word
token**, which is path-based so a string literal like `'select'`
can't be mistaken for the keyword). `plan_shortid_autofill`
(`db.rs`) implements the **user-chosen Option B**: when the column
list omits a `shortid` column, materialise the row source by
running it as a query, generate a distinct id per row via the
existing `generate_shortid_batch` (deduped against stored values),
and reconstruct a parameterised multi-row INSERT. Uniform for
VALUES and SELECT; handles multiple omitted shortids (one batch per
column). `serial` stays engine-filled via rowid. `history.log`
keeps the original line, never the rewrite (§11).
**Arity guard (`18d34d0`):** the auto-fill path read exactly
`listed_columns.len()` cells per row — a column/value arity
mismatch silently dropped extra columns or read out of range. Now:
if the materialised statement's `column_count() != listed_count`,
skip auto-fill and run verbatim so the engine reports the mismatch
(a friendly pre-flight is 3i).
Minor deviation from the plan: the plan's 3d step said "turn on
`writes_user_listed_column`". We did **not** flip that flag — the
worker only needs the column *names*, collected by role in
`build_sql_insert`. The flag drives completion-narrowing of VALUES
against the listed columns, which nothing needs yet. It remains
`false`; flipping it is a separate completion enhancement, not a
blocker.
### 3e — SQL `UPDATE` (`53808ed`)
New `src/dsl/grammar/sql_update.rs`: `SQL_UPDATE_SHAPE =
<table> SET col = sql_expr (',' …)* [WHERE sql_expr] [';']`. No
`--all-rows` rail (ADR-0030 §12). `Command::SqlUpdate { sql,
target_table }`, `Request::RunSqlUpdate`, `do_sql_update` (execute
verbatim, re-persist target, history). 3e surfaces the
**affected-row count only**; precise rows are RETURNING (3g).
**Two findings that matter for later sub-phases:**
1. **Cross-cut diagnostics are NOT automatically free.** The
schema-existence + predicate-warning passes (`mod.rs`) build
their scope from Tables idents whose **role is `"table_name"`**
(the pre-pass at `schema_existence_diagnostics`). A bespoke role
(`update_target_table`) left the SET/WHERE columns unchecked
(`diag_keys` returned `[]`). Fix: the UPDATE target uses the
shared `"table_name"` role. **3f's DELETE target must do the
same** to get the predicate diagnostics on its WHERE for free.
2. **Render guard.** A column-less `UpdateResult` would render a
misleading `(no rows)` band. `handle_dsl_update_success` now
skips `render_data_table` when `result.data.columns.is_empty()`.
The DSL UPDATE always has columns, so it's unaffected. Covered
by app-level tests both ways.
`sql_select::WHERE_CLAUSE` is now `pub(crate)` so the DML
statements reuse the exact predicate clause.
## §4. Sub-phase 3f — the next job (DELETE + cascade)
Per the plan (`docs/plans/20260520-adr-0033-phase-3.md`, "Sub-phase
3f"): new `src/dsl/grammar/sql_delete.rs` (`DELETE FROM table
[WHERE] [';']`), `Command::SqlDelete` / `Request::RunSqlDelete` /
`do_sql_delete`, cascade-summary pre-count (ADR-0033 §7), the
`format_cascade_summary` formatter **shared** with the DSL
`do_delete`, and **multi-table persistence** (target + every
cascade-affected child).
**Heads-up — the WHERE-byte-extraction problem is tractable for
DELETE.** In 3e I worried that a flat token path can't distinguish
a statement-level WHERE from a subquery's WHERE (a `where` token
can appear before *or* after the statement one). That ambiguity
comes from UPDATE's `SET` clause possibly holding a subquery-with-
WHERE *before* the statement WHERE. **DELETE has no SET** — nothing
before the statement WHERE can contain a subquery — so the
statement WHERE is simply the **first `where` Word token after the
target table**, and the predicate text is `source[where_start..]`
(trim trailing `;`). The R2 invariant
(`DELETE … WHERE x IN (SELECT … WHERE …)`) is fine: the nested
subquery WHERE is *inside* the predicate, which is exactly what the
cascade pre-count wants to inject. So mirror the
`build_sql_insert` row_source / `build_sql_update` extraction: find
the first `where` token, capture the clause text into
`Command::SqlDelete`, and inject it into
`SELECT <pk> FROM <target> <where_clause>` for each child
pre-count. When there is no WHERE, the pre-count is unbounded
(all target rows).
Other 3f gotchas from the plan's DA gate: the cascade pre-count
must run **before** the DELETE (the rows being counted are the ones
about to vanish); cascade-affected child CSVs must all be
re-persisted; and the SQL path's per-relationship summary must
match the DSL path's on the same schema/data (shared formatter).
## §5. Established patterns (reuse these in 3f3i)
- **Dev entry word per sub-phase.** SQL DML is isolated behind
`sqlinsert` / `sql_update` (and `sql_delete` next) entry words,
`CommandCategory::Advanced` in `REGISTRY`
(`src/dsl/grammar/mod.rs`). The `build_sql_*` ast-builder
reconstructs the real keyword (`insert`/`update`/`delete`) +
matched tail. **3j removes all dev words** and makes
`insert`/`update`/`delete` shared DSL/SQL entry words; 3j must
also de-dup the completion entry-word lists once a word appears
twice in `REGISTRY` (flagged in ADR-0033 Amendment 1).
- **`table_name` role for any target whose WHERE/SET columns need
schema diagnostics** (see §3e finding 1).
- **Static-vs-const in grammar files.** A `Node` referenced *by
value* in a `static [...]` array must be `const` (so it inlines);
a `Node` referenced via `&NODE` can be `static`. Getting this
wrong gives "cannot move out of a shared reference" (hit twice
in 3e).
- **Worker result + render.** SQL DML reuses the DSL
`CommandOutcome::{Insert,Update,Delete}` and the
`handle_dsl_*_success` renderers. Any new `Command` variant must
be added to: `command.rs` `verb()` + `target_table()`, the
`runtime.rs` dispatch, `app.rs` `build_translate_context`, and
the `tests/typing_surface/mod.rs` `command_kind_label` match
(all are non-exhaustive checks that will fail to compile until
covered — a useful forcing function).
## §6. Escalations settled this session (do not re-litigate)
- **Identifiers-first candidate ordering** (ADR-0022 Amendment 2):
schema identifiers sort *before* keywords, globally — the user
explicitly chose this over the handoff-14 keywords-first
invariant, after seeing that long SQL keyword runs pushed column
names off the single-row, window-scrolled candidate line. A
**two-line hint box** is recorded as a deferred follow-up.
- **3d SELECT shortid strategy = Option B** (materialise + dedup +
reinsert), user-confirmed.
- **`auto_column_overridden` WARNING stays INSERT-only** (the plan
default). 3e did not extend it to UPDATE; if 3f/3i wants to, the
plan says escalate.
## §7. Still deferred (tracked, not lost)
- **RETURNING** (3g) — precise DML row output; 3e/3f surface counts
(+ cascade summary) only until then.
- **UPSERT `ON CONFLICT`** (3h), **diagnostics** `insert_arity_mismatch`
/ `auto_column_overridden` / `not_null_missing` (3i),
**dispatch wiring / dev-word removal** (3j), **verification
sweep** (3k).
- **Old-project migration** (from handoff 30 §4) — the pre-
`check_expr` 3-column `__rdbms_playground_columns` schema; the
user deferred a migration story to "the end". Targeted fix when
the migration framework (ADR-0015 Iter 6) lands.
- **Two-line hint box** (ADR-0022 Amendment 2).
- **`writes_user_listed_column`** flag (see §3d) — VALUES-against-
listed-columns completion narrowing.
## §8. Process pins (unchanged, still binding)
- **Confirm every commit.** Propose the message and wait for the
go-ahead. No auto-commit at sub-phase gates.
- **Push is the user's step.** Never push; never prompt about it.
- **No AI attribution** in commits (global rule).
- **Test-first.** Reproduce a bug with a FAILING test before
fixing; for features, exit-gate tests before "done". This is
also how the real DA findings surfaced this session — write the
cross-cut/edge test and watch it fail.
- **Core walker changes** (`walk_seq` / `walk_choice` /
`walk_repeated`) need explicit user OK before coding. (F5 and the
3d-era `walk_repeated` change both went through this.)
- **Escalate ambiguity; never classify work out of scope without
user confirmation.**
## §9. How to take over
1. **Read, in order:** this file →
`docs/adr/0033-sql-dml-grammar.md` (esp. §7 cascade + §2
UPDATE, already shipped) →
`docs/plans/20260520-adr-0033-phase-3.md` "Sub-phase 3f" →
`CLAUDE.md`.
2. **Baseline:**
```
cargo test # expect 1524 passing / 0 failing / 1 ignored
cargo clippy --all-targets -- -D warnings # clean
```
3. **Start 3f** per §4. Mirror `sql_update.rs` for the grammar and
`do_sql_update` for the worker shell; the new work is the
cascade pre-count + the shared `format_cascade_summary` refactor
+ multi-table persistence. Use the `table_name` role for the
DELETE target. Capture the WHERE clause text via the first
`where` token (tractable for DELETE — §4).
4. **Do the DA pass for real** (§2). On a sub-phase this size,
expect to find at least one untested branch or edge case;
if you don't, look harder.
5. **Escalate** anything not settled in ADR-0033 / the plan.