//! SQL `UPDATE` grammar (ADR-0033 §2, sub-phase 3e). //! //! Grammar-as-text (ADR-0030 §4): the walker validates that the //! `UPDATE` is in the supported subset; the worker executes the //! validated SQL text and re-persists the target table's CSV //! (ADR-0030 §11). The shape here is the post-`UPDATE` portion — //! the entry-word dispatch consumes the leading `UPDATE` keyword //! before this shape walks. `update` is a shared entry word //! (sub-phase 3j): this `Advanced` SQL shape and the `Simple` DSL //! update node both register under `update`. //! //! Scope (3e): ` SET assignment_list [ WHERE … ]`, the //! `__rdbms_*` target rejection, and the shared `sql_expr` on both //! the assignment RHS and the WHERE predicate. There is no //! `--all-rows` rail — a SQL `UPDATE` without `WHERE` runs as //! written (ADR-0030 §12). `RETURNING` (3g) lands later. use crate::dsl::grammar::shared::SET_VALUE; use crate::dsl::grammar::sql_select::{RETURNING_CLAUSE, WHERE_CLAUSE, reject_internal_table}; use crate::dsl::grammar::{IdentSource, Node, Word}; static COMMA: Node = Node::Punct(','); /// The `UPDATE` target table. `__rdbms_*` rejected (ADR-0030 §6 / /// ADR-0033 §1). `writes_table` populates `current_table` / /// `current_table_columns` so the `SET` columns and the WHERE /// predicate get column completion against the target. /// /// Uses the shared `table_name` role (not a bespoke one) so the /// Phase-2 schema-existence + predicate-warning passes collect it /// as a scope binding and check the SET / WHERE columns against it /// for free (ADR-0033 §2's "cross-cut from Phase-2 machinery"). const TARGET_TABLE: Node = Node::Ident { source: IdentSource::Tables, role: "table_name", validator: Some(reject_internal_table), highlight_override: None, writes_table: true, writes_column: false, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }; /// The column on the left of one `SET col = expr` assignment. /// /// `writes_column: true` resolves the column's type into /// `current_column` (and frames the value-slot hint via /// `pending_value_column`), so the RHS `SET_VALUE` lookahead can /// dispatch the column-typed slot for a lone literal (ADR-0036 /// Phase 3a). const ASSIGN_COLUMN: Node = Node::Ident { source: IdentSource::Columns, role: "update_set_column", validator: None, highlight_override: None, writes_table: false, writes_column: true, writes_user_listed_column: false, writes_table_alias: false, writes_cte_name: false, writes_projection_alias: false, }; /// `column_name '=' ` — the RHS is the boundary-aware /// `SET_VALUE` slot (ADR-0036 Phase 3a): a lone literal routes to the /// column-typed slot (live per-column hint + numeric-shape highlight, /// shared with the DSL), while any expression — arithmetic, a /// literal-prefixed form, `CASE`, function calls, scalar subqueries — /// falls through to the full `sql_expr` grammar (ADR-0031), which the /// engine evaluates at execution time. static ASSIGNMENT_NODES: &[Node] = &[ASSIGN_COLUMN, Node::Punct('='), SET_VALUE]; static ASSIGNMENT: Node = Node::Seq(ASSIGNMENT_NODES); /// `assignment ( ',' assignment )*`. const ASSIGNMENT_LIST: Node = Node::Repeated { inner: &ASSIGNMENT, separator: Some(&COMMA), min: 1, }; static SQL_UPDATE_TAIL_NODES: &[Node] = &[ TARGET_TABLE, Node::Word(Word::keyword("set")), ASSIGNMENT_LIST, Node::Optional(&WHERE_CLAUSE), Node::Optional(&RETURNING_CLAUSE), Node::Optional(&Node::Punct(';')), ]; /// The post-`UPDATE` portion of a SQL `UPDATE` statement /// (ADR-0033 §2): `
SET col = expr (',' col = expr)* /// [ WHERE … ] [ ';' ]`. /// /// The entry-word dispatch consumes the leading `UPDATE` keyword /// before this shape walks, so a `CommandNode` references it as /// its `shape` (sub-phase 3e registers a development entry word; /// sub-phase 3j wires the shared `update` entry word). pub static SQL_UPDATE_SHAPE: Node = Node::Seq(SQL_UPDATE_TAIL_NODES); // ================================================================= // Tests — grammar accept/reject for the post-`UPDATE` tail. // ================================================================= #[cfg(test)] mod tests { use super::SQL_UPDATE_SHAPE; use crate::dsl::walker::context::WalkContext; use crate::dsl::walker::driver::{NodeWalkResult, walk_node}; use crate::dsl::walker::outcome::MatchedPath; /// Walk `input` against the UPDATE tail. Returns `true` only /// when the walk matches *and* consumes all of `input` /// (trailing whitespace allowed). Schemaless: the shape is /// structural, so table/column idents match by shape and /// `reject_internal_table` still fires on `__rdbms_*`. fn walks(input: &str) -> bool { let mut ctx = WalkContext::new(); let mut path = MatchedPath::new(); let mut per_byte = Vec::new(); match walk_node( input, 0, &SQL_UPDATE_SHAPE, &mut ctx, &mut path, &mut per_byte, ) { NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(), _ => false, } } fn good(input: &str) { assert!(walks(input), "{input:?} should be a valid UPDATE tail"); } fn bad(input: &str) { assert!( !walks(input), "{input:?} should NOT walk as a complete UPDATE tail" ); } #[test] fn single_assignment_with_where() { good("t set v = 'x' where id = 1"); good("t set v = 1 where id = 1;"); } #[test] fn multi_assignment() { good("t set a = 1, b = 2 where id = 1"); good("orders set total = 0, note = 'void' where id = 7"); } #[test] fn no_where_runs_across_all_rows() { // ADR-0030 §12: no `--all-rows` rail — a SQL UPDATE without // WHERE is structurally valid. good("t set active = false"); good("t set a = 1, b = 2"); } #[test] fn assignment_rhs_admits_sql_expr() { good("t set total = price * quantity where id = 1"); good("t set v = case when x > 0 then x else 0 end"); good("t set v = (select max(other) from other_table) where id = 1"); } #[test] fn returning_tail_admitted() { // 3g: optional RETURNING projection_list tail. good("t set v = 1 where id = 1 returning *"); good("t set v = 1 returning id, v"); good("t set v = 1 where id = 1 returning v as new_v;"); } #[test] fn internal_target_table_rejected() { bad("__rdbms_playground_columns set a = 1"); bad("__rdbms_playground_relationships set a = 1 where id = 1"); } #[test] fn structurally_incomplete_or_wrong_rejected() { // Missing SET. bad("t where id = 1"); bad("t"); // SET with no assignment. bad("t set"); bad("t set where id = 1"); // Assignment missing RHS. bad("t set v ="); // Trailing comma with no following assignment. bad("t set a = 1,"); } }