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:
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user