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
+52
View File
@@ -1187,6 +1187,20 @@ impl App {
"parse.error",
detail = parse_error_message(&err)
));
// ADR-0033 Amendment 3: combine the DSL error with a
// pointer to advanced mode when the same line would
// run as SQL there. Only in simple mode (a one-shot
// `:` or persistent advanced submission uses the SQL
// surface already). This mirrors the live hint and
// covers SQL constructs that surface only on submit
// (e.g. `delete … returning`, where the live hint
// shows WHERE-completion rather than an error).
if submission_mode == Mode::Simple
&& let Some(note) =
crate::input_render::advanced_alternative_note(input, &self.schema_cache)
{
self.note_error(note);
}
// ADR-0021 §2: append the usage block (if a
// known command-entry keyword was consumed) or
// the available-commands fallback (§5).
@@ -2326,6 +2340,44 @@ mod tests {
);
}
#[test]
fn simple_mode_submit_of_sql_construct_appends_advanced_pointer() {
// ADR-0033 Amendment 3: submitting a line in simple mode that
// fails as DSL but would run as SQL in advanced mode appends
// the `advanced_mode.also_valid_sql` pointer to the parse
// error — keeping the DSL detail and pointing at advanced
// mode. Multi-row VALUES is a definite DSL error and valid SQL
// (no schema needed).
let mut app = App::new();
type_str(&mut app, "insert into T values (1, 2), (3, 4)");
let actions = submit(&mut app);
assert!(actions.is_empty(), "the bad line must not dispatch");
let has_pointer = app
.output
.iter()
.any(|l| l.kind == OutputKind::Error && l.text.contains("advanced mode"));
assert!(
has_pointer,
"expected the advanced-mode pointer on submit; output:\n{}",
error_lines(&app),
);
}
#[test]
fn simple_mode_submit_of_pure_dsl_error_has_no_advanced_pointer() {
// A DSL error that is *not* valid SQL either (unknown command)
// must not carry the advanced-mode pointer — there is nothing
// to switch modes for.
let mut app = App::new();
type_str(&mut app, "frobulate widgets");
let _ = submit(&mut app);
let has_pointer = app
.output
.iter()
.any(|l| l.text.contains("valid as SQL in advanced mode"));
assert!(!has_pointer, "unknown command must not point at advanced mode");
}
#[test]
fn enter_in_advanced_mode_dispatches_select_with_advanced_tag() {
// The pre-ADR-0030 placeholder echoed any advanced-mode