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:
+154
-2
@@ -360,12 +360,23 @@ mod tests {
|
||||
use crate::dsl::value::Value;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
// These helpers parse in **Simple mode** — the DSL surface
|
||||
// (ADR-0003). The tests in this module exercise the DSL
|
||||
// grammar (`insert`/`update`/`delete` Forms A/B/C, the
|
||||
// `--all-rows` rail, DDL, app commands), all of which are
|
||||
// canonical in Simple mode. Since sub-phase 3j made
|
||||
// `insert`/`update`/`delete` shared entry words (ADR-0033 §2,
|
||||
// Amendment 3), parsing these in Advanced mode would route the
|
||||
// overlap to the SQL command variants; the SQL surface is
|
||||
// covered by `tests/sql_*.rs` instead. No SQL-only command
|
||||
// (`select`/`with`) is tested through these helpers.
|
||||
fn ok(input: &str) -> Command {
|
||||
parse_command(input).unwrap_or_else(|e| panic!("expected ok for {input:?}, got {e:?}"))
|
||||
parse_command_in_mode(input, Mode::Simple)
|
||||
.unwrap_or_else(|e| panic!("expected ok for {input:?}, got {e:?}"))
|
||||
}
|
||||
|
||||
fn err(input: &str) -> ParseError {
|
||||
parse_command(input).expect_err("expected parse error")
|
||||
parse_command_in_mode(input, Mode::Simple).expect_err("expected parse error")
|
||||
}
|
||||
|
||||
fn err_message(input: &str) -> String {
|
||||
@@ -1163,6 +1174,147 @@ mod tests {
|
||||
assert!(matches!(e, ParseError::Invalid { .. }), "got {e:?}");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// Sub-phase 3j — shared-entry-word dispatch (ADR-0033 §2,
|
||||
// Amendment 1 / Amendment 3).
|
||||
//
|
||||
// `insert` / `update` / `delete` are *shared* entry words: a
|
||||
// `Simple` DSL node and an `Advanced` SQL node both register
|
||||
// under each. A command's identity is the outcome of the
|
||||
// mode-rooted grammar path:
|
||||
// - Advanced mode tries the SQL shape first and falls back to
|
||||
// the DSL shape only when the SQL shape *structurally* can't
|
||||
// match (e.g. the DSL-only `--all-rows` flag). A content
|
||||
// rejection (a `__rdbms_*` target) on the SQL shape is
|
||||
// surfaced, never masked by the DSL fallback.
|
||||
// - Simple mode commits the DSL shape; it points the user at
|
||||
// advanced mode ("this is SQL") only when the input is
|
||||
// SQL-only (the DSL shape structurally mismatches and the SQL
|
||||
// shape matches — e.g. a `returning` tail). A DSL command
|
||||
// that is merely incomplete or has a bad value still commits
|
||||
// the DSL node so the user sees DSL completion / DSL errors.
|
||||
// The §6/§7 parity guarantees mean the two variants execute to
|
||||
// identical effects for an overlapping input.
|
||||
// =====================================================
|
||||
|
||||
#[test]
|
||||
fn advanced_ambiguous_insert_routes_to_sql() {
|
||||
assert!(matches!(
|
||||
parse_command_in_mode("insert into Orders values (1, 2)", Mode::Advanced),
|
||||
Ok(Command::SqlInsert { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_ambiguous_update_routes_to_sql() {
|
||||
assert!(matches!(
|
||||
parse_command_in_mode(
|
||||
"update Orders set total = 0 where id = 1",
|
||||
Mode::Advanced,
|
||||
),
|
||||
Ok(Command::SqlUpdate { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_ambiguous_delete_routes_to_sql() {
|
||||
assert!(matches!(
|
||||
parse_command_in_mode("delete from Orders where id = 1", Mode::Advanced),
|
||||
Ok(Command::SqlDelete { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_dsl_only_delete_falls_back_to_dsl() {
|
||||
// `--all-rows` is DSL-only; the SQL DELETE shape can't consume
|
||||
// the trailing flag, so dispatch falls back to the DSL node.
|
||||
assert_eq!(
|
||||
parse_command_in_mode("delete from Orders --all-rows", Mode::Advanced).unwrap(),
|
||||
Command::Delete {
|
||||
table: "Orders".to_string(),
|
||||
filter: RowFilter::AllRows,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_mode_data_commands_reject_internal_tables() {
|
||||
// ADR-0030 §6 ("every table-source slot") / `/runda` finding
|
||||
// B: the DSL data-command target slots reject `__rdbms_*`
|
||||
// internal tables in simple mode too — matching the SQL
|
||||
// grammar. Without this, simple-mode DML could read/write the
|
||||
// internal metadata tables while advanced-mode SQL rejected
|
||||
// them.
|
||||
for input in [
|
||||
"insert into __rdbms_playground_columns values (1)",
|
||||
"update __rdbms_playground_columns set x = 1 where id = 1",
|
||||
"delete from __rdbms_playground_columns where id = 1",
|
||||
"show data __rdbms_playground_columns",
|
||||
"show table __rdbms_playground_relationships",
|
||||
] {
|
||||
assert!(
|
||||
parse_command_in_mode(input, Mode::Simple).is_err(),
|
||||
"internal table must be rejected in simple mode: {input:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_internal_table_insert_is_rejected_not_fallen_back() {
|
||||
// The SQL insert's `reject_internal_table` rail must surface
|
||||
// even though the DSL insert node lacks it: a content
|
||||
// rejection commits the SQL candidate rather than falling
|
||||
// through to the DSL node that would accept it.
|
||||
assert!(
|
||||
parse_command_in_mode(
|
||||
"insert into __rdbms_playground_columns values (1)",
|
||||
Mode::Advanced,
|
||||
)
|
||||
.is_err(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_dsl_delete_stays_dsl() {
|
||||
assert_eq!(
|
||||
parse_command_in_mode("delete from Orders where id = 1", Mode::Simple).unwrap(),
|
||||
Command::Delete {
|
||||
table: "Orders".to_string(),
|
||||
filter: RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sql_only_entry_word_points_at_advanced_mode() {
|
||||
// A SQL-only *entry word* (`select`) has no DSL form, so
|
||||
// simple mode emits the "this is SQL" hint at the parse level
|
||||
// (ADR-0030 §2).
|
||||
match parse_command_in_mode("select Name from Orders", Mode::Simple) {
|
||||
Err(ParseError::Invalid { message, .. }) => assert!(
|
||||
message.contains("advanced"),
|
||||
"expected the this-is-SQL hint, got: {message}",
|
||||
),
|
||||
other => panic!("expected the this-is-SQL hint, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_shared_word_with_sql_construct_is_a_dsl_parse_error() {
|
||||
// `returning` is SQL-only, but `delete` is a *shared* entry
|
||||
// word, so simple mode commits the DSL shape and surfaces a
|
||||
// DSL parse error (ADR-0033 Amendment 3). The "(valid as SQL
|
||||
// in advanced mode)" pointer is added at the hint layer
|
||||
// (input_render), not in the parsed command/error here.
|
||||
assert!(matches!(
|
||||
parse_command_in_mode(
|
||||
"delete from Orders where id = 1 returning *",
|
||||
Mode::Simple,
|
||||
),
|
||||
Err(ParseError::Invalid { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_data_command() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user