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:
claude@clouddev1
2026-05-26 22:48:46 +00:00
parent 8c3b13b313
commit 49ea03b0d5
7 changed files with 376 additions and 32 deletions
+13 -11
View File
@@ -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