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:
+13
-13
@@ -69,7 +69,7 @@ fn run_update(
|
||||
rt: &tokio::runtime::Runtime,
|
||||
input: &str,
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
match parse_command(input).expect("parse sql_update") {
|
||||
match parse_command(input).expect("parse update") {
|
||||
Command::SqlUpdate { sql, target_table, returning } => {
|
||||
rt.block_on(db.run_sql_update(sql, Some(input.to_string()), target_table, returning))
|
||||
}
|
||||
@@ -79,8 +79,8 @@ fn run_update(
|
||||
|
||||
#[test]
|
||||
fn parse_path_lowers_sql_update_to_command() {
|
||||
let command = parse_command("sql_update Orders set total = 0 where id = 1")
|
||||
.expect("sql_update parses in advanced mode");
|
||||
let command = parse_command("update Orders set total = 0 where id = 1")
|
||||
.expect("update parses in advanced mode");
|
||||
match command {
|
||||
Command::SqlUpdate { sql, target_table, .. } => {
|
||||
assert_eq!(sql, "update Orders set total = 0 where id = 1");
|
||||
@@ -96,7 +96,7 @@ fn single_column_update_with_where_persists() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'old'), (2, 'keep')", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set v = 'new' where id = 1")
|
||||
let result = run_update(&db, &rt, "update t set v = 'new' where id = 1")
|
||||
.expect("update runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row updated");
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
@@ -117,7 +117,7 @@ fn multi_column_update_persists() {
|
||||
&["id"],
|
||||
);
|
||||
seed(&db, &rt, "insert into t (id, a, b) values (1, 0, 'x')", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set a = 9, b = 'y' where id = 1")
|
||||
let result = run_update(&db, &rt, "update t set a = 9, b = 'y' where id = 1")
|
||||
.expect("multi-col update runs");
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
@@ -131,7 +131,7 @@ fn update_without_where_runs_across_all_rows() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, active) values (1, true), (2, true)", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set active = false")
|
||||
let result = run_update(&db, &rt, "update t set active = false")
|
||||
.expect("unfiltered update runs");
|
||||
assert_eq!(result.rows_affected, 2, "all rows updated");
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
@@ -150,7 +150,7 @@ fn update_with_sql_expr_in_set() {
|
||||
&["id"],
|
||||
);
|
||||
seed(&db, &rt, "insert into t (id, price, qty, total) values (1, 6, 7, 0)", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set total = price * qty where id = 1")
|
||||
let result = run_update(&db, &rt, "update t set total = price * qty where id = 1")
|
||||
.expect("expression update runs");
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
@@ -169,7 +169,7 @@ fn update_with_subquery_in_set() {
|
||||
let result = run_update(
|
||||
&db,
|
||||
&rt,
|
||||
"sql_update t set v = (select max(n) from other) where id = 1",
|
||||
"update t set v = (select max(n) from other) where id = 1",
|
||||
)
|
||||
.expect("subquery-set update runs");
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
@@ -185,7 +185,7 @@ fn update_matching_no_rows_is_ok() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'keep')", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set v = 'x' where id = 999")
|
||||
let result = run_update(&db, &rt, "update t set v = 'x' where id = 999")
|
||||
.expect("no-match update is a success");
|
||||
assert_eq!(result.rows_affected, 0, "no rows matched");
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
@@ -198,7 +198,7 @@ fn update_appends_literal_line_to_history() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'old')", "t");
|
||||
let input = "sql_update t set v = 'new' where id = 1";
|
||||
let input = "update t set v = 'new' where id = 1";
|
||||
run_update(&db, &rt, input).expect("update runs");
|
||||
let body = std::fs::read_to_string(project.path().join("history.log"))
|
||||
.expect("history.log present");
|
||||
@@ -215,7 +215,7 @@ fn update_returning_yields_modified_columns() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'old'), (2, 'keep')", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set v = 'new' where id = 1 returning id, v")
|
||||
let result = run_update(&db, &rt, "update t set v = 'new' where id = 1 returning id, v")
|
||||
.expect("UPDATE … RETURNING runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row updated");
|
||||
assert_eq!(result.data.columns, vec!["id".to_string(), "v".to_string()]);
|
||||
@@ -230,7 +230,7 @@ fn update_returning_recovers_bare_column_type() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, active) values (1, false)", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set active = true where id = 1 returning active")
|
||||
let result = run_update(&db, &rt, "update t set active = true where id = 1 returning active")
|
||||
.expect("UPDATE … RETURNING active runs");
|
||||
assert_eq!(result.data.column_types, vec![Some(Type::Bool)], "bool type recovered");
|
||||
assert_eq!(result.data.rows[0][0], Some("true".to_string()));
|
||||
@@ -246,7 +246,7 @@ fn update_returning_matching_no_rows_is_ok_and_empty() {
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'keep')", "t");
|
||||
let result = run_update(&db, &rt, "sql_update t set v = 'x' where id = 999 returning id, v")
|
||||
let result = run_update(&db, &rt, "update t set v = 'x' where id = 999 returning id, v")
|
||||
.expect("no-match UPDATE … RETURNING is a success");
|
||||
assert_eq!(result.rows_affected, 0, "no rows matched");
|
||||
assert!(result.data.rows.is_empty(), "no rows returned");
|
||||
|
||||
Reference in New Issue
Block a user