feat: ADR-0036 Phase 3a — live typed-slot hints + highlighting for SQL SET values
Wire the DSL's column-typed value slots into the advanced-mode SQL
UPDATE/UPSERT `SET col = <rhs>` value position so a learner gets the same
per-column hint ("for `Email`: type a quoted string") and live numeric-
shape mismatch highlight the simple-mode DSL gives.
Discriminate literal-vs-expression with a boundary-aware lookahead
(shared::SET_VALUE), NOT the naive `Choice(typed-slot, sql_expr)` the ADR
originally sketched: the walker's Choice is first-match-wins with no
backtrack, so a typed slot would greedily match the leading `1` of `1 + 2`
and commit, regressing valid SQL (e.g. the existing `values (1, 1 + 2)`
test). The lookahead peeks the whole value position: a literal routes to
the typed slot only when it fills the position up to the next
`,`/`)`/`;`/`where`/`returning`/end; everything else falls through to the
full sql_expr grammar unchanged. The SET column ident gets
`writes_column: true` so `current_column` drives the slot + hint.
Scope: Phase 3a covers UPDATE's assignment list and INSERT's ON CONFLICT
DO UPDATE SET. Phase 3b (INSERT VALUES — needs a per-position grammar
restructure + multi-row) is deferred. Records ADR-0036 Amendment 1 with
the mechanism correction + the 3a/3b split.
Tests: 1939 passing (+5), 0 failed, 0 skipped, 1 ignored; clippy clean.
This commit is contained in:
@@ -456,3 +456,114 @@ pub fn column_value_list(ctx: &WalkContext) -> Node {
|
||||
}
|
||||
Node::Seq(Box::leak(children.into_boxed_slice()))
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Advanced-mode SQL `SET col = <rhs>` value slot (ADR-0036 Phase 3a)
|
||||
// =================================================================
|
||||
//
|
||||
// A SQL UPDATE / UPSERT `SET col = <rhs>` value position routes a
|
||||
// *lone literal* (a string / number / `null` / `true` / `false` that
|
||||
// fills the whole position up to the next `,` / `where` / `returning`
|
||||
// / `;` / end-of-input) to the column-typed slot — so the learner gets
|
||||
// the same per-column hint + numeric-shape highlight the DSL gives —
|
||||
// and routes anything else (an expression: arithmetic, a literal-
|
||||
// prefixed form like `1 + 2`, a function call, a scalar subquery, a
|
||||
// column / `excluded` reference) to the full `sql_expr` grammar,
|
||||
// unchanged (ADR-0030 §4).
|
||||
//
|
||||
// Discrimination is by *lookahead*, NOT `Choice([typed_slot,
|
||||
// sql_expr])`. A naive Choice would let the typed slot greedily match
|
||||
// the leading literal of `1 + 2` and commit, leaving `+ 2` dangling —
|
||||
// the walker's `Choice` is first-match-wins with no cross-branch
|
||||
// backtrack — turning a valid expression into a parse error. The
|
||||
// lookahead peeks the whole value position first, so a literal routes
|
||||
// to the typed slot only when it is the *entire* value (ADR-0036
|
||||
// Amendment 1).
|
||||
|
||||
/// Lookahead factory for a SQL `SET col = <rhs>` value position
|
||||
/// (ADR-0036 Phase 3a). Routes a lone literal to the column-typed slot
|
||||
/// (live hint + highlight) and everything else to `sql_expr`.
|
||||
fn set_value_node(_ctx: &WalkContext, source: &str, pos: usize) -> Node {
|
||||
if set_rhs_is_lone_literal(source, pos) {
|
||||
// The column type was resolved into `current_column` by the
|
||||
// preceding `SET` column ident (`writes_column: true`), so the
|
||||
// typed slot drives the per-column hint + highlight.
|
||||
Node::DynamicSubgrammar(current_column_value)
|
||||
} else {
|
||||
// An expression — the engine evaluates it; keep the full
|
||||
// `sql_expr` surface. Returned from a `fn` (not a `const`) so
|
||||
// the reference doesn't enter the sql_expr ⇄ sql_select
|
||||
// const-evaluation cycle (see the matching note in `data.rs`).
|
||||
Node::Subgrammar(&crate::dsl::grammar::sql_expr::SQL_OR_EXPR)
|
||||
}
|
||||
}
|
||||
|
||||
/// The SQL `SET col = <rhs>` value slot (ADR-0036 Phase 3a). Shared by
|
||||
/// `sql_update`'s assignment list and the `sql_insert` UPSERT
|
||||
/// `DO UPDATE SET`.
|
||||
pub const SET_VALUE: Node = Node::Lookahead(set_value_node);
|
||||
|
||||
/// True when the `SET` RHS starting at `pos` is empty, a partial string
|
||||
/// still being typed, or exactly one complete literal token that fills
|
||||
/// the value position (the next token is a position boundary). Such
|
||||
/// positions route to the column-typed slot so its hint/highlight fire;
|
||||
/// everything else is an expression and routes to `sql_expr`.
|
||||
///
|
||||
/// An empty RHS counts as lone-literal so the typed-slot hint shows
|
||||
/// while the cursor sits right after `=`. A signed number (`-5`) counts
|
||||
/// too: `consume_number_literal` — the same consumer the slot's
|
||||
/// `NumberLit` uses — folds the leading sign into the literal, so the
|
||||
/// slot can match it.
|
||||
fn set_rhs_is_lone_literal(source: &str, pos: usize) -> bool {
|
||||
use crate::dsl::walker::lex_helpers::{
|
||||
consume_ident, consume_number_literal, consume_string_literal, skip_whitespace,
|
||||
};
|
||||
let p = skip_whitespace(source, pos);
|
||||
if p >= source.len() {
|
||||
return true; // empty RHS — show the typed-slot hint
|
||||
}
|
||||
// A string literal — single-quoted (SQL strings). Complete (a
|
||||
// boundary must follow) or a partial one still being typed (route
|
||||
// to the slot so the hint persists while typing).
|
||||
if source.as_bytes()[p] == b'\'' {
|
||||
return match consume_string_literal(source, p) {
|
||||
Some(((_, end), _)) => next_is_set_boundary(source, end),
|
||||
None => true, // unterminated string, mid-typing
|
||||
};
|
||||
}
|
||||
// A number literal (incl. a leading sign) that fills the position.
|
||||
if let Some((_, end)) = consume_number_literal(source, p) {
|
||||
return next_is_set_boundary(source, end);
|
||||
}
|
||||
// `null` / `true` / `false` filling the position.
|
||||
if let Some((s, e)) = consume_ident(source, p) {
|
||||
let word = &source[s..e];
|
||||
if word.eq_ignore_ascii_case("null")
|
||||
|| word.eq_ignore_ascii_case("true")
|
||||
|| word.eq_ignore_ascii_case("false")
|
||||
{
|
||||
return next_is_set_boundary(source, e);
|
||||
}
|
||||
return false; // any other identifier → column ref / function → expression
|
||||
}
|
||||
false // punctuation / sign-then-space / `(` → expression
|
||||
}
|
||||
|
||||
/// True when the next non-whitespace token after `pos` ends a `SET`
|
||||
/// value position: `,` (next assignment), `)` / `;` / end-of-input, or
|
||||
/// a `where` / `returning` clause keyword.
|
||||
fn next_is_set_boundary(source: &str, pos: usize) -> bool {
|
||||
use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
let p = skip_whitespace(source, pos);
|
||||
let Some(b) = source.as_bytes().get(p) else {
|
||||
return true; // end of input
|
||||
};
|
||||
if *b == b',' || *b == b';' || *b == b')' {
|
||||
return true;
|
||||
}
|
||||
if let Some((s, e)) = consume_ident(source, p) {
|
||||
let word = &source[s..e];
|
||||
return word.eq_ignore_ascii_case("where") || word.eq_ignore_ascii_case("returning");
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//! (3g), and `ON CONFLICT … ` UPSERT (3h) land in later
|
||||
//! sub-phases.
|
||||
|
||||
use crate::dsl::grammar::shared::SET_VALUE;
|
||||
use crate::dsl::grammar::sql_expr;
|
||||
use crate::dsl::grammar::sql_select::{
|
||||
RETURNING_CLAUSE, SQL_SELECT_COMPOUND, WHERE_CLAUSE, reject_internal_table,
|
||||
@@ -146,30 +147,31 @@ const OPTIONAL_CONFLICT_TARGET: Node = Node::Optional(&Node::Seq(CONFLICT_TARGET
|
||||
/// The column on the left of one `DO UPDATE SET col = expr`
|
||||
/// assignment. Mirrors `sql_update`'s `ASSIGN_COLUMN` shape (same
|
||||
/// `update_set_column` role so it gets the same column completion /
|
||||
/// diagnostics against the target table).
|
||||
/// diagnostics against the target table). `writes_column: true`
|
||||
/// resolves the column type into `current_column` so the RHS
|
||||
/// `SET_VALUE` lookahead can dispatch the typed slot for a lone
|
||||
/// literal (ADR-0036 Phase 3a).
|
||||
const UPSERT_SET_COLUMN: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "update_set_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_column: true,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// `column '=' sql_expr` — the RHS reuses the shared expression
|
||||
/// grammar (ADR-0031), so `excluded.col`, literals, operators,
|
||||
/// `CASE`, and function calls are all admitted. `excluded` is the
|
||||
/// would-have-been-inserted row (ADR-0033 §9); it parses as a
|
||||
/// `column '=' <value>` — the RHS is the boundary-aware `SET_VALUE`
|
||||
/// slot (ADR-0036 Phase 3a), shared with `sql_update`: a lone literal
|
||||
/// routes to the column-typed slot (live hint + highlight) while an
|
||||
/// expression — `excluded.col`, operators, `CASE`, function calls —
|
||||
/// falls through to the full `sql_expr` grammar (ADR-0031). `excluded`
|
||||
/// is the would-have-been-inserted row (ADR-0033 §9); it parses as a
|
||||
/// qualified ref via `sql_expr` and the engine resolves it.
|
||||
static UPSERT_ASSIGNMENT_NODES: &[Node] = &[
|
||||
UPSERT_SET_COLUMN,
|
||||
Node::Punct('='),
|
||||
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||
];
|
||||
static UPSERT_ASSIGNMENT_NODES: &[Node] = &[UPSERT_SET_COLUMN, Node::Punct('='), SET_VALUE];
|
||||
static UPSERT_ASSIGNMENT: Node = Node::Seq(UPSERT_ASSIGNMENT_NODES);
|
||||
// `const` — used by value in `DO_UPDATE_NODES` (static-vs-const
|
||||
// rule: a `Node` referenced by value in a `static [...]` must be
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! `--all-rows` rail — a SQL `UPDATE` without `WHERE` runs as
|
||||
//! written (ADR-0030 §12). `RETURNING` (3g) lands later.
|
||||
|
||||
use crate::dsl::grammar::sql_expr;
|
||||
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};
|
||||
|
||||
@@ -44,28 +44,33 @@ const TARGET_TABLE: Node = Node::Ident {
|
||||
};
|
||||
|
||||
/// 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: false,
|
||||
writes_column: true,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// `column_name '=' sql_expr` — the RHS reuses the shared
|
||||
/// expression grammar (ADR-0031), so literals, operators, `CASE`,
|
||||
/// function calls, and scalar subqueries are all admitted; the
|
||||
/// engine evaluates them at execution time.
|
||||
static ASSIGNMENT_NODES: &[Node] = &[
|
||||
ASSIGN_COLUMN,
|
||||
Node::Punct('='),
|
||||
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||
];
|
||||
/// `column_name '=' <value>` — 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 )*`.
|
||||
|
||||
Reference in New Issue
Block a user