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
-73
@@ -32,8 +32,14 @@ use crate::dsl::walker::outcome::{MatchedItem, MatchedKind, MatchedPath};
|
||||
|
||||
const TABLE_NAME_EXISTING: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
// Reject `__rdbms_*` internal tables at the table-source slot
|
||||
// (ADR-0030 §6 — "every table-source slot"), matching the SQL
|
||||
// grammar's `reject_internal_table`. Without this, simple-mode DSL
|
||||
// data commands could read/write the internal metadata tables
|
||||
// even though advanced-mode SQL rejects them (ADR-0033
|
||||
// Amendment 3 / `/runda` finding B).
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
validator: Some(sql_select::reject_internal_table),
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
@@ -49,8 +55,10 @@ writes_projection_alias: false,
|
||||
/// dispatch typed slots per column.
|
||||
const TABLE_NAME_INSERT: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
// Reject `__rdbms_*` internal tables (ADR-0030 §6; `/runda`
|
||||
// finding B) — see `TABLE_NAME_EXISTING`.
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
validator: Some(sql_select::reject_internal_table),
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
@@ -224,8 +232,12 @@ const INSERT_SHAPE: Node = Node::Seq(INSERT_NODES);
|
||||
/// can resolve column types (Phase D).
|
||||
const TABLE_NAME_WRITES: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
// Reject `__rdbms_*` internal tables (ADR-0030 §6; `/runda`
|
||||
// finding B) — see `TABLE_NAME_EXISTING`. Shared by `update`,
|
||||
// `delete`, and `show data`, so all three reject the internal
|
||||
// metadata tables, matching the SQL grammar.
|
||||
role: "table_name",
|
||||
validator: None,
|
||||
validator: Some(sql_select::reject_internal_table),
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
@@ -856,14 +868,10 @@ fn build_select(_path: &MatchedPath, source: &str) -> Result<Command, Validation
|
||||
}
|
||||
|
||||
/// Build `Command::SqlInsert` from a validated SQL `INSERT`
|
||||
/// (ADR-0033 §1, sub-phase 3b). Extracts the target table from
|
||||
/// the matched path so the worker re-persists the right CSV.
|
||||
///
|
||||
/// Dev-scaffold detail: the entry word is `sqlinsert` (not valid
|
||||
/// SQL), so the statement is reconstructed as `insert` + the
|
||||
/// matched tail. Sub-phase 3j wires the real `insert` entry word,
|
||||
/// at which point this collapses to `source.trim()` like
|
||||
/// `build_select`.
|
||||
/// (ADR-0033 §1). Extracts the target table from the matched path
|
||||
/// so the worker re-persists the right CSV. `insert` is now the
|
||||
/// real (shared) entry word, so the validated `source` runs
|
||||
/// verbatim — like `build_select` (sub-phase 3j).
|
||||
fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
let target_table = path
|
||||
.items
|
||||
@@ -938,13 +946,10 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Everything after the entry word is the `INTO …` tail; prefix
|
||||
// the real `insert` keyword for the engine.
|
||||
let tail = path
|
||||
.items
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("insert {}", tail.trim());
|
||||
// The entry word is the real `insert` keyword (sub-phase 3j),
|
||||
// so the validated line runs verbatim (grammar-as-text,
|
||||
// ADR-0030 §4) — no keyword reconstruction.
|
||||
let sql = source.trim().to_string();
|
||||
Ok(Command::SqlInsert {
|
||||
sql,
|
||||
target_table,
|
||||
@@ -966,13 +971,10 @@ fn path_has_returning(path: &MatchedPath) -> bool {
|
||||
}
|
||||
|
||||
/// Build `Command::SqlUpdate` from a validated SQL `UPDATE`
|
||||
/// (ADR-0033 §2, sub-phase 3e). Extracts the target table from the
|
||||
/// matched path so the worker re-persists the right CSV.
|
||||
///
|
||||
/// Dev-scaffold detail: the entry word is `sql_update` (not valid
|
||||
/// SQL), so the statement is reconstructed as `update` + the
|
||||
/// matched tail. Sub-phase 3j wires the real `update` entry word,
|
||||
/// at which point this collapses to `source.trim()`.
|
||||
/// (ADR-0033 §2). Extracts the target table from the matched path
|
||||
/// so the worker re-persists the right CSV. `update` is now the
|
||||
/// real (shared) entry word, so the validated `source` runs
|
||||
/// verbatim (sub-phase 3j).
|
||||
fn build_sql_update(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
// The UPDATE target is the first `table_name` ident (it
|
||||
// precedes any table referenced inside a SET / WHERE subquery).
|
||||
@@ -986,11 +988,7 @@ fn build_sql_update(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tail = path
|
||||
.items
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("update {}", tail.trim());
|
||||
let sql = source.trim().to_string();
|
||||
Ok(Command::SqlUpdate {
|
||||
sql,
|
||||
target_table,
|
||||
@@ -999,17 +997,13 @@ fn build_sql_update(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
}
|
||||
|
||||
/// Build `Command::SqlDelete` from a validated SQL `DELETE`
|
||||
/// (ADR-0033 §1/§7, sub-phase 3f). Extracts the target table from
|
||||
/// the matched path so the worker re-persists the right CSV and
|
||||
/// snapshots the right inbound children for cascade diffing. No
|
||||
/// WHERE clause is captured — the worker executes the verbatim SQL
|
||||
/// and never inspects the predicate (Amendment 2).
|
||||
///
|
||||
/// Dev-scaffold detail: the entry word is `sql_delete` (not valid
|
||||
/// SQL), so the statement is reconstructed as `delete` + the matched
|
||||
/// tail (which opens at `from`). Sub-phase 3j wires the real
|
||||
/// `delete` entry word, at which point this collapses to
|
||||
/// `source.trim()`.
|
||||
/// (ADR-0033 §1/§7). Extracts the target table from the matched
|
||||
/// path so the worker re-persists the right CSV and snapshots the
|
||||
/// right inbound children for cascade diffing. No WHERE clause is
|
||||
/// captured — the worker executes the verbatim SQL and never
|
||||
/// inspects the predicate (Amendment 2). `delete` is now the real
|
||||
/// (shared) entry word, so the validated `source` runs verbatim
|
||||
/// (sub-phase 3j).
|
||||
fn build_sql_delete(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
// The DELETE target is the first `table_name` ident (it precedes
|
||||
// any table referenced inside a WHERE subquery).
|
||||
@@ -1023,11 +1017,7 @@ fn build_sql_delete(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tail = path
|
||||
.items
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("delete {}", tail.trim());
|
||||
let sql = source.trim().to_string();
|
||||
Ok(Command::SqlDelete {
|
||||
sql,
|
||||
target_table,
|
||||
@@ -1110,51 +1100,46 @@ pub static WITH: CommandNode = CommandNode {
|
||||
help_id: None,
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
|
||||
/// SQL `INSERT` development scaffold (ADR-0033 sub-phase 3b–3i).
|
||||
/// SQL `INSERT` — the `Advanced`-category node of the shared
|
||||
/// `insert` entry word (ADR-0033 §2, Amendment 1, sub-phase 3j).
|
||||
///
|
||||
/// Registered under the temporary entry word `sqlinsert` so the
|
||||
/// SQL INSERT grammar and execution path can be exercised in
|
||||
/// isolation, WITHOUT yet making `insert` a shared DSL/SQL entry
|
||||
/// word. Sharing `insert` is sub-phase 3j, which depends on
|
||||
/// `shortid` auto-fill (3d) so advanced-mode DSL inserts keep
|
||||
/// parity rather than regressing through an incomplete SQL path.
|
||||
/// This scaffold (entry word + reconstruction in `build_sql_insert`)
|
||||
/// is removed when 3j wires the real `insert` entry word.
|
||||
/// `insert` is a shared entry word: this `Advanced` SQL node and
|
||||
/// the `Simple` DSL [`INSERT`] node both register under `insert`.
|
||||
/// In Advanced mode the dispatcher (`walker::walk` / `decide`)
|
||||
/// tries this SQL node first and falls back to the DSL node when
|
||||
/// the SQL shape does not match; in Simple mode only the DSL node
|
||||
/// is reachable (Amendment 3 — command identity is the mode-rooted
|
||||
/// grammar-path outcome).
|
||||
pub static SQL_INSERT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("sqlinsert"),
|
||||
entry: Word::keyword("insert"),
|
||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||
ast_builder: build_sql_insert,
|
||||
help_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
/// SQL `UPDATE` development scaffold (ADR-0033 sub-phase 3e).
|
||||
/// SQL `UPDATE` — the `Advanced` node of the shared `update` word.
|
||||
///
|
||||
/// Registered under the temporary entry word `sql_update` so the
|
||||
/// SQL UPDATE grammar and execution path can be exercised in
|
||||
/// isolation, WITHOUT yet making `update` a shared DSL/SQL entry
|
||||
/// word. Sharing `update` is sub-phase 3j. This scaffold (entry
|
||||
/// word + reconstruction in `build_sql_update`) is removed when 3j
|
||||
/// wires the real `update` entry word.
|
||||
/// ADR-0033 §2 / Amendment 1, sub-phase 3j. Pairs with the `Simple`
|
||||
/// DSL [`UPDATE`] node; dispatch is SQL-first / DSL-fallback in
|
||||
/// Advanced mode, DSL-only in Simple.
|
||||
pub static SQL_UPDATE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("sql_update"),
|
||||
entry: Word::keyword("update"),
|
||||
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
||||
ast_builder: build_sql_update,
|
||||
help_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
/// SQL `DELETE` development scaffold (ADR-0033 sub-phase 3f).
|
||||
/// SQL `DELETE` — the `Advanced` node of the shared `delete` word.
|
||||
///
|
||||
/// Registered under the temporary entry word `sql_delete` so the
|
||||
/// SQL DELETE grammar and execution path (including cascade-summary
|
||||
/// parity) can be exercised in isolation, WITHOUT yet making
|
||||
/// `delete` a shared DSL/SQL entry word. Sharing `delete` is
|
||||
/// sub-phase 3j. This scaffold (entry word + reconstruction in
|
||||
/// `build_sql_delete`) is removed when 3j wires the real `delete`
|
||||
/// entry word.
|
||||
/// ADR-0033 §2 / Amendment 1, sub-phase 3j. Pairs with the `Simple`
|
||||
/// DSL [`DELETE`] node; dispatch is SQL-first / DSL-fallback in
|
||||
/// Advanced mode, DSL-only in Simple. In Advanced mode `delete from t
|
||||
/// --all-rows` falls back to the DSL node (the SQL shape has no
|
||||
/// `--all-rows`).
|
||||
pub static SQL_DELETE: CommandNode = CommandNode {
|
||||
entry: Word::keyword("sql_delete"),
|
||||
entry: Word::keyword("delete"),
|
||||
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
||||
ast_builder: build_sql_delete,
|
||||
help_id: None,
|
||||
|
||||
Reference in New Issue
Block a user