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:
+58
-18
@@ -973,6 +973,24 @@ pub fn invalid_ident_at_cursor(
|
||||
input: &str,
|
||||
cursor: usize,
|
||||
cache: &SchemaCache,
|
||||
) -> Option<InvalidIdent> {
|
||||
invalid_ident_at_cursor_in_mode(input, cursor, cache, Mode::Advanced)
|
||||
}
|
||||
|
||||
/// Mode-aware [`invalid_ident_at_cursor`].
|
||||
///
|
||||
/// The slot's expected set is computed in `mode`, so a simple-mode
|
||||
/// caller doesn't get the advanced (SQL) grammar's view of a shared
|
||||
/// `insert`/`update`/`delete` entry word — e.g. it won't flag `rows`
|
||||
/// as an "unknown column" in `update T … --all-rows`, which is a DSL
|
||||
/// flag in simple mode rather than the SQL expression `- -all - rows`
|
||||
/// (ADR-0033 Amendment 3).
|
||||
#[must_use]
|
||||
pub fn invalid_ident_at_cursor_in_mode(
|
||||
input: &str,
|
||||
cursor: usize,
|
||||
cache: &SchemaCache,
|
||||
mode: Mode,
|
||||
) -> Option<InvalidIdent> {
|
||||
let cursor = cursor.min(input.len());
|
||||
let bytes = input.as_bytes();
|
||||
@@ -1000,7 +1018,7 @@ pub fn invalid_ident_at_cursor(
|
||||
return None;
|
||||
}
|
||||
let leading = &input[..start];
|
||||
let expected = expected_at(leading, Mode::Advanced);
|
||||
let expected = expected_at(leading, mode);
|
||||
if expected.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -1094,6 +1112,17 @@ mod tests {
|
||||
.map_or_else(Vec::new, |c| c.candidates.into_iter().map(|c| c.text).collect())
|
||||
}
|
||||
|
||||
/// Simple-mode completion candidates — the DSL surface
|
||||
/// (ADR-0003). Used by tests of DSL-only completion (the
|
||||
/// `--all-rows` flag, the DSL value-literal slots), which since
|
||||
/// sub-phase 3j must run in Simple mode: `insert`/`update`/
|
||||
/// `delete` are shared entry words (ADR-0033 Amendment 3) and
|
||||
/// Advanced mode surfaces the SQL grammar's completions instead.
|
||||
fn cands_simple(input: &str, cursor: usize) -> Vec<String> {
|
||||
candidates_at_cursor_in_mode(input, cursor, &SchemaCache::default(), Mode::Simple)
|
||||
.map_or_else(Vec::new, |c| c.candidates.into_iter().map(|c| c.text).collect())
|
||||
}
|
||||
|
||||
fn cand_kinds_with(
|
||||
input: &str,
|
||||
cursor: usize,
|
||||
@@ -1178,14 +1207,17 @@ mod tests {
|
||||
// a `,` (more assignments), `where` (where clause),
|
||||
// or `--all-rows` (flag). Punctuation isn't surfaced;
|
||||
// `where` and `--all-rows` should appear.
|
||||
let cs = cands("update T set Name='hi' ", 23);
|
||||
// `--all-rows` is a DSL-only rail (Simple mode); in Advanced
|
||||
// mode `update`/`delete` route to the SQL grammar, which has
|
||||
// no such flag (ADR-0033 Amendment 3).
|
||||
let cs = cands_simple("update T set Name='hi' ", 23);
|
||||
assert!(cs.contains(&"where".to_string()), "got {cs:?}");
|
||||
assert!(cs.contains(&"--all-rows".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_filter_position_offers_where_and_all_rows() {
|
||||
let cs = cands("delete from T ", 14);
|
||||
let cs = cands_simple("delete from T ", 14);
|
||||
assert!(cs.contains(&"where".to_string()), "got {cs:?}");
|
||||
assert!(cs.contains(&"--all-rows".to_string()), "got {cs:?}");
|
||||
}
|
||||
@@ -1195,12 +1227,18 @@ mod tests {
|
||||
// Hint-panel colouring distinguishes flags from
|
||||
// keywords (amber vs purple) — flags get their own
|
||||
// CandidateKind so the renderer can apply tok_flag.
|
||||
let kinds = candidates_at_cursor("delete from T ", 14, &SchemaCache::default())
|
||||
.expect("some completion")
|
||||
.candidates
|
||||
.into_iter()
|
||||
.map(|c| (c.text, c.kind))
|
||||
.collect::<Vec<_>>();
|
||||
// Simple mode: `--all-rows` is the DSL rail.
|
||||
let kinds = candidates_at_cursor_in_mode(
|
||||
"delete from T ",
|
||||
14,
|
||||
&SchemaCache::default(),
|
||||
Mode::Simple,
|
||||
)
|
||||
.expect("some completion")
|
||||
.candidates
|
||||
.into_iter()
|
||||
.map(|c| (c.text, c.kind))
|
||||
.collect::<Vec<_>>();
|
||||
let flag = kinds
|
||||
.iter()
|
||||
.find(|(t, _)| t == "--all-rows")
|
||||
@@ -1210,7 +1248,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn flag_candidates_filter_by_partial_prefix() {
|
||||
let cs = cands("delete from T --", 16);
|
||||
let cs = cands_simple("delete from T --", 16);
|
||||
assert!(cs.contains(&"--all-rows".to_string()), "got {cs:?}");
|
||||
}
|
||||
|
||||
@@ -1327,7 +1365,9 @@ mod tests {
|
||||
// `true`, `false` as Tab candidates — actively
|
||||
// misleading at a slot where the user is more likely
|
||||
// entering a number / text / date. Suppress.
|
||||
let cs = cands("insert into T values (", 22);
|
||||
// DSL value-literal slot (Simple mode); in Advanced mode
|
||||
// `insert` routes to the SQL grammar (ADR-0033 Amendment 3).
|
||||
let cs = cands_simple("insert into T values (", 22);
|
||||
assert!(cs.is_empty(), "got misleading candidates {cs:?}");
|
||||
}
|
||||
|
||||
@@ -1337,15 +1377,15 @@ mod tests {
|
||||
// completion applies — `n` → `null`, `tr` → `true`,
|
||||
// `fa` → `false`.
|
||||
assert_eq!(
|
||||
cands("insert into T values (n", 23),
|
||||
cands_simple("insert into T values (n", 23),
|
||||
vec!["null".to_string()],
|
||||
);
|
||||
assert_eq!(
|
||||
cands("insert into T values (tr", 24),
|
||||
cands_simple("insert into T values (tr", 24),
|
||||
vec!["true".to_string()],
|
||||
);
|
||||
assert_eq!(
|
||||
cands("insert into T values (fa", 24),
|
||||
cands_simple("insert into T values (fa", 24),
|
||||
vec!["false".to_string()],
|
||||
);
|
||||
}
|
||||
@@ -1355,21 +1395,21 @@ mod tests {
|
||||
// Comma-separated value positions all hit the same slot
|
||||
// signature. `insert into T values (1, ` → expected:
|
||||
// null/true/false/number/string. Suppress.
|
||||
let cs = cands("insert into T values (1, ", 25);
|
||||
let cs = cands_simple("insert into T values (1, ", 25);
|
||||
assert!(cs.is_empty(), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_set_value_slot_suppresses() {
|
||||
// `update T set col=` is also a value-literal slot.
|
||||
let cs = cands("update T set col=", 17);
|
||||
let cs = cands_simple("update T set col=", 17);
|
||||
assert!(cs.is_empty(), "got {cs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn where_value_slot_suppresses() {
|
||||
// `where col=` is also a value-literal slot.
|
||||
let cs = cands("delete from T where col=", 24);
|
||||
let cs = cands_simple("delete from T where col=", 24);
|
||||
assert!(cs.is_empty(), "got {cs:?}");
|
||||
}
|
||||
|
||||
@@ -2166,7 +2206,7 @@ mod tests {
|
||||
// ADR-0033 §9: `excluded.|` inside a DO UPDATE action
|
||||
// completes to the INSERT target table's columns.
|
||||
let cache = two_table_schema();
|
||||
let input = "sqlinsert into a (id, name) values (1, 'x') \
|
||||
let input = "insert into a (id, name) values (1, 'x') \
|
||||
on conflict (id) do update set name = excluded.";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user