grammar+walker: 3j — shared insert/update/delete entry words (ADR-0033 §2 / Amendments 1 & 3)

Wire `insert`/`update`/`delete` as shared DSL/SQL entry words through the
category-grouped dispatcher (ADR-0033 Amendment 1): the Advanced SQL nodes
move off the dev words (`sqlinsert`/`sql_update`/`sql_delete`) to the real
keywords, registered alongside the Simple DSL nodes. Remove the dev-word
scaffold; collapse build_sql_{insert,update,delete} to source.trim();
de-duplicate the two REGISTRY entry-word listing sites.

Dispatch model (ADR-0033 Amendment 3, written this round):
- A command is the mode-rooted grammar-path outcome; identity is intrinsic.
  Advanced mode tries SQL first, falling back to the Simple DSL command when
  no SQL branch matches a token (`delete … --all-rows` falls back;
  `update … --all-rows` does not — the SET expression absorbs it, harmless
  since the engine treats `--all-rows` as a comment).
- Simple mode commits the DSL candidate for a shared word, surfacing the real
  DSL error; bare "this is SQL" is reserved for SQL-only entry words
  (`select`/`with`). A content rejection on the SQL candidate (internal
  table) is committed, never masked by the DSL fallback.

Combined DSL-error + advanced-SQL pointer (ADR-0033 Amendment 3): a Simple-mode
definite DSL error that would run as SQL in advanced mode gains the
`advanced_mode.also_valid_sql` suffix — in the live hint (ambient_hint_in_mode)
and on submit (dispatch_dsl), via the shared advanced_alternative_note — so the
actionable DSL fix and the mode pointer coexist (submit covers constructs that
surface only on submit, e.g. `delete … returning`).

Internal-table rejection symmetrised (/runda finding B, ADR-0030 §6): the DSL
data-command target slots (insert/update/delete/show data/show table) gained
reject_internal_table, so `__rdbms_*` tables are refused in Simple mode too —
previously only the advanced SQL grammar rejected them.

Mode-awareness: classify_input_with_schema_in_mode and
invalid_ident_at_cursor_in_mode stop leaking the advanced SQL view into
simple-mode hints for shared words.

Tests: dev-word inputs migrated to the real words (advanced); DSL grammar /
completion / phase-D / db tests parse in Simple mode (the DSL surface); replay
keeps its advanced-mode model (one stale assertion fixed); dispatcher routing,
combined-pointer, and internal-table tests added. Suite 1626 pass / 0 fail /
1 ignored; clippy --all-targets -D warnings clean.

Defer M4 (execution-time mode side-channel; tracked in requirements.md) to its
own ADR.
This commit is contained in:
claude@clouddev1
2026-05-23 21:13:39 +00:00
parent c16196fc7f
commit d5c7f63513
22 changed files with 956 additions and 314 deletions
+186
View File
@@ -1270,6 +1270,192 @@ parity is preserved. The user confirmed this deferral.
- The handoff-31 §4 "WHERE-byte-extraction is tractable for DELETE"
heads-up is moot — no extraction happens.
## Amendment 3 — Command identity is intrinsic; execution-mode is side-channel (deferred) (2026-05-23)
This amendment **clarifies the command-identity model** that §2 and
Amendment 1 implement, **corrects a false premise in the
implementation plan's sub-phase 3j exit gate**, and **records a
deferred follow-up** (the execution-time mode side-channel). It was
written during sub-phase 3j — wiring the shared `insert` / `update` /
`delete` entry words — after the dispatch model's interaction with the
existing test suite surfaced a question about what a "command" *is* and
how input mode relates to it. Recorded with explicit user approval
before any 3j code landed.
### The model — a command is a mode-rooted grammar-path outcome
A command is the typed outcome of a grammar path **rooted at the input
mode**: `simple → update → …` or `advanced → update → …`. Its identity
is **intrinsic** — determined by *which grammar matched*, not by a mode
flag carried on one shared command type. In Advanced mode the
dispatcher (Amendment 1) tries the Advanced (SQL) candidate(s) first and
**falls back to the Simple (DSL) candidate** when no Advanced branch
recognizes a token; the fallback produces the **Simple** command.
Worked example (the `--all-rows` fall-through): `delete from T
--all-rows` in Advanced mode — no Advanced (SQL) branch recognizes
`--all-rows` (the SQL `DELETE` has no expression slot after the table
to absorb it), so the path falls back to `simple → delete → … →
all-rows` and yields `Command::Delete { filter: AllRows }` (the DSL
command), exactly as it would in Simple mode. The mode rooted the path;
the command that came out is the Simple one.
*Counter-example (no fall-through):* `update T set x = 42 --all-rows`
in Advanced mode does **not** fall back — the SQL `UPDATE`'s
`SET <expr>` greedily consumes `--all-rows` as the expression
`42 - -all - rows` (with `all`/`rows` as column refs), so the SQL shape
matches and `Command::SqlUpdate` is produced. Whether `--all-rows` is a
DSL flag or part of a SQL expression is decided by which grammar wins,
not by special-casing the token. (At execution this is harmless: the
engine treats `--all-rows` as a SQL line comment, so the statement runs
as `update T set x = 42` — all rows — the same effect as the DSL flag.)
### Simple mode commits the DSL candidate; an advanced-mode pointer combines
In **Simple mode** a shared entry word **always commits its DSL
candidate**, so the user sees DSL completion and the *real* DSL error
(with its position) — not a bare "this is SQL" that discards the
actionable detail. A plain `ValidationFailed`/`Mismatch` "this is SQL"
dispatch outcome is reserved for entry words that have **no** DSL form
(`select` / `with`); for those the DSL surface has nothing to offer, so
the simple-mode gate points at advanced mode (ADR-0030 §2).
To keep the SQL-discoverability the original §2 envisioned *without*
losing the DSL fix, the rendering layer **combines** them: when a
simple-mode line is a *definite* DSL error (not merely incomplete) and
the same line would parse in advanced mode, the DSL error prose is
suffixed with the `advanced_mode.also_valid_sql` pointer — e.g.
`for \`Name\`: Type a quoted string … (valid as SQL in advanced mode —
\`mode advanced\` or prefix \`:\`)`. The DSL detail and the mode hint
coexist (`input_render::ambient_hint_in_mode` /
`advanced_alternative_note`). Mid-typing (incomplete) input is not
suffixed, to avoid noise during normal DSL entry. This supersedes the
draft of this amendment's earlier suggestion that a SQL-shaped line in
simple mode emits a *bare* "this is SQL" hint for shared words.
### Overlapping inputs produce two distinct commands — both are tested
For a **fully-overlapping** shape — `insert into T values (…)` is valid
in both grammars — Simple mode yields `Command::Insert` (a typed AST,
DSL execution) and Advanced mode yields `Command::SqlInsert` (validated
text, SQL execution). This is **correct, not a defect**: the two
commands *do the same thing but execute differently* (ADR-0030 §4 — the
DSL lowers to a typed AST; SQL is grammar-as-text run verbatim). Because
they are distinct commands, **each is tested in the mode that produces
it** — DSL grammar tests run in Simple mode; the SQL command variants
are tested in Advanced mode.
### Correction to the plan's 3j exit gate
The implementation plan's sub-phase 3j exit gate says "all existing DSL
`INSERT`/`UPDATE`/`DELETE` tests still green — unmodified inputs,
unmodified outputs", and its scope-out says "observable behaviour must
be identical before and after 3j". That rested on a **false premise**:
that the existing DSL DML tests run in Simple mode. They do not — the
common helpers (`ok`/`err`/`parse`) call `parse_command`, which
**defaults to Advanced mode**. Today that is harmless because
`insert`/`update`/`delete` have no SQL competitor, so they route to the
DSL command even in Advanced mode. Once they become **shared** entry
words, §2's Advanced-mode **SQL-first** rule routes the overlap to the
SQL command (`Command::Sql*`), changing the parsed variant those tests
assert.
The corrected invariant: **Simple-mode behaviour is unchanged; Advanced
mode is SQL-first per §2; the DSL grammar is tested in Simple mode (its
canonical surface, ADR-0003); both command variants are tested in their
producing mode.** No *production* behaviour regresses — the §6 (shortid
auto-fill) and §7 (cascade summary) parity promises keep the two paths
observably equivalent for overlapping inputs; only the
implementation-internal `Command` variant differs, and only in Advanced
mode.
### Replay uses the same parser, in advanced mode — and needs no per-line mode
A consequence worth stating explicitly, because it is easy to
over-think (and a prior reading of this work did). **Replay parses
each recorded line with the same schema-aware parser the interactive
path uses, in advanced mode** (the full surface) — it skips nothing
and simplifies nothing. The only difference from an interactive line
is the mode argument: interactive uses the user's current mode,
replay fixes it to advanced.
This is correct and complete **without** the per-line execution-mode
side-channel below, because of the §6 (shortid auto-fill) and §7
(cascade-summary) parity guarantees: a valid overlapping command
produces *identical effects* whether it lowers to the DSL variant or
the SQL variant. So replaying a log of valid commands in advanced
mode is identical to typing those lines interactively in advanced
mode — and, by parity, identical in effect to the simple-mode entry
that originally produced them. (Replay only ever sees lines that
already executed successfully: `history.log` is success-only, and
ADR-0034's deferred journal replays `ok` lines only.)
The one observable nuance is purely in **error reporting for an
invalid line** — which only arises from a hand-built script, never
from a real journal. Such a line is still rejected and not applied;
*where* the rejection lands just follows the grammar, exactly as
interactively: an `insert … values …` line is SQL in advanced mode,
so a wrong column-type value is rejected by the engine at execute
time rather than by a DSL typed slot at parse time. This is **not**
replay using a lesser parser — it is the same advanced-mode parse a
user would get typing the line. Replay therefore does **not** depend
on the deferred side-channel below.
### Execution-time mode is side-channel — deferred to its own ADR
Independent of command *identity*, every command should — at **execution
time** — know which of three modes it ran under: `simple`, `advanced`,
or `advanced-one-shot` (the `:` escape from Simple mode, ADR-0003), so
execution can adjust **output** without changing **identity** (e.g. a
Simple-mode `create table` echoing the generated SQL when run in
Advanced mode, while staying silent in Simple mode).
Today this exists only as a **rendering** side-channel
(`OutputLine.mode_at_submission`, consumed by the echo-line mode tag in
`ui.rs`). The `Mode` enum is two-way (`Simple` / `Advanced`); the
one-shot distinction is a transient "effective mode" collapsed at
submission; and neither `Action::ExecuteDsl` nor the database worker
carries any mode. Wiring an **execution-time** mode side-channel —
widening `Mode` to the three-way distinction and threading it through
the `Action` → worker interface — is **out of scope for Phase 3** and is
**deferred to its own ADR** (user-confirmed). It is *not* required for
Phase 3's dispatch to be correct: the routing and the `--all-rows`
fall-through above are complete on the two-way mode. This amendment
forward-references that future ADR so the requirement is not lost.
### Consequences of the amendment
- **No code or grammar change** from this amendment itself — it records
the model that the dispatch (Amendment 1) already implements and
corrects the plan's test-mode premise.
- The 3j test migration **moves the DSL grammar / completion tests to
Simple mode** (inputs and `Command::Insert`/`Update`/`Delete`
assertions unchanged), **keeps the `--all-rows` fall-through tests in
Advanced mode** (they validate the fallback to the Simple command),
relies on the migrated `tests/sql_*.rs` for the SQL command variants
in Advanced mode, and adds dispatcher routing tests
(Advanced + structurally-ambiguous → SQL; Advanced + `--all-rows` →
DSL fallback; Simple + a SQL-only *entry word* `select` → "this is
SQL"; Simple + a shared word with a SQL-only *construct* → DSL error,
carrying the combined pointer at the rendering layer).
- The **combined pointer** lives in `input_render::ambient_hint_in_mode`
(live typing) and `App::dispatch_dsl` (submit), both via the shared
`advanced_alternative_note`, so a Simple-mode definite DSL error that
would run as SQL gains the `advanced_mode.also_valid_sql` suffix in
both surfaces (the submit path covers SQL constructs that surface
only on submit, e.g. `delete … returning`). Found via the `/runda`
round.
- **Internal-table rejection symmetrised** (`/runda` finding B,
ADR-0030 §6): the DSL data-command target slots (`insert` / `update` /
`delete` / `show data` / `show table`) gained
`reject_internal_table`, so `__rdbms_*` tables are refused in Simple
mode too — previously only the advanced SQL grammar rejected them, so
a simple-mode DML could touch the internal metadata tables.
- The **execution-time mode side-channel + three-way `Mode`** is tracked
as a future ADR; `OutputLine.mode_at_submission` remains the only mode
side-channel until then. No structure built in 3j assumes mode is
irrelevant to execution.
## See also
- ADR-0005 — the ten-type vocabulary INSERT works with.
+1 -1
View File
File diff suppressed because one or more lines are too long