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
+80 -9
View File
@@ -16,8 +16,12 @@ validation + offending-value retention; the same capture-at-parse technique
on the SET assignment list — `capture_set_literals` in `data.rs`
classifying each top-level RHS literal-vs-expression, validating literals in
`do_sql_update`, and reading them in `user_value_for_column`; `WHERE` is not
validated, execution stays verbatim). Phase 3 (completion
hinting/highlighting — the only part needing a grammar change) pending.
validated, execution stays verbatim). **Phase 3a implemented 2026-05-26**
— live typed-slot hints + numeric-shape highlighting for advanced-mode
`UPDATE`/UPSERT `SET col = <literal>` value positions, via a
**boundary-aware lookahead** (not the naive `Choice` this ADR originally
sketched in §5 — see **Amendment 1**). Phase 3b (`INSERT … VALUES` typed
slots — needs a per-position grammar restructure + multi-row) pending.
**Augments** **ADR-0030 §4** and **ADR-0033 §10** — it does **not**
supersede them and does **not** change the execution model. Advanced-mode
@@ -265,13 +269,17 @@ execution), only its `Result` is used.
verbatim update; `user_value_for_column` reads them so a constraint error
names the offending value. `WHERE` is deliberately not validated (§2).
- **Phase 3 — completion hinting / highlighting.** This is the *only*
part that needs a grammar change: a `Choice(typed-literal-slot,
sql_expr)` at each value position (reusing the DSL's live
`column_value_list` / `TypedValueSlot`s — `data.rs:141`/`189`/`269`),
so the column type drives a live hint and a mismatch highlights while
typing. When Phase 3 lands, the typed slot supersedes Phase 1's
classification of literals (the validation/enrichment built on top is
unaffected — that is the only throwaway, by design).
part that needs a grammar change: a typed-literal slot vs `sql_expr` at
each value position (reusing the DSL's live `column_value_list` /
`TypedValueSlot`s — `data.rs:141`/`189`/`269`), so the column type
drives a live hint and a mismatch highlights while typing. When Phase 3
lands, the typed slot supersedes Phase 1/2's classification of literals
(the validation/enrichment built on top is unaffected — that is the
only throwaway, by design). **The literal-vs-expression discriminator
is a boundary-aware *lookahead*, not a naive `Choice(typed-slot,
sql_expr)` — see Amendment 1, which corrects this section's mechanism
and splits Phase 3 into 3a (`SET`, implemented 2026-05-26) and 3b
(`VALUES`, pending).**
### 6. Non-goals
@@ -315,6 +323,69 @@ execution), only its `Result` is used.
validation/enrichment built on it is permanent; only the detection is
provisional — a deliberate, documented small throwaway.
## Amendment 1 — Phase 3 mechanism is a boundary-aware lookahead, not a naive `Choice`; Phase 3 split into 3a/3b (2026-05-26)
**Status:** Accepted (agreed with the user in conversation, 2026-05-26).
**Phase 3a implemented the same day.**
§5 Phase 3 sketched the mechanism as "a `Choice(typed-literal-slot,
sql_expr)` at each value position." Implementation found that sketch is
**wrong as written** and would regress valid SQL, so it is corrected here.
**Why the naive `Choice` is broken.** The walker's `Node::Choice` is
first-match-wins with **no cross-branch backtracking** once a branch has
committed a `Matched` (a later failure in the enclosing `Seq` does not
re-enter the Choice). At, say, an `int` column, the value `1 + 2`:
- Branch 0 (the typed slot) matches just the `1` and commits, leaving
`+ 2` dangling — the enclosing tuple/assignment then fails on `+`.
- The Choice never falls through to `sql_expr`, so a **valid, currently
parsing SQL expression is rejected**.
This is not hypothetical: `tests/sql_insert.rs::sql_insert_expression_value_is_not_validated_and_runs`
exercises exactly `values (1, 1 + 2)`. Putting `sql_expr` first instead
makes the typed slot unreachable (sql_expr matches bare literals too),
defeating the purpose. So the discriminator must know whether a literal
**fills the whole value position** before choosing the typed slot.
**The correction.** Discriminate by a **boundary-aware lookahead**
(`shared::SET_VALUE` → `set_value_node`): peek the value position and
route to the column-typed slot only when it is empty, a partial string
still being typed, or a single complete literal token whose next token is
a position boundary (`,` / `)` / `;` / `where` / `returning` / end);
otherwise route to `Subgrammar(sql_expr)`. The empty case still routes to
the slot so the per-column hint shows while the cursor sits right after
`=`. A leading sign folds into the literal (the slot's `NumberLit` uses
the same `consume_number_literal` that eats a `-`), so signed literals get
typed treatment too. `Node::Lookahead` already exists and is used the same
way by `insert_first_paren` (`data.rs`). The validation/enrichment from
Phases 12 is unchanged; only the *live-feedback detection* uses this
lookahead — consistent with §5's note that Phase 3's detection is the one
deliberate throwaway.
**Phase 3 split into 3a + 3b.** The two halves differ structurally:
- **Phase 3a (implemented) — `UPDATE` / UPSERT `SET col = <rhs>`.** Low
risk: the preceding `SET` column ident gets `writes_column: true` so
`current_column` (and the `pending_value_column` hint framing) is set
per assignment; the RHS becomes `shared::SET_VALUE`. Covers both
`sql_update`'s assignment list and `sql_insert`'s `ON CONFLICT … DO
UPDATE SET`. Mismatch examples now caught **live** (e.g. `set k = 3.14`
at an `int` column), matching what simple mode already does — earlier,
better feedback than Phase 2's execution-time catch.
- **Phase 3b (pending) — `INSERT … VALUES (…)`.** Harder: the values list
is `Repeated(VALUE_EXPR)` with **no per-position column identity**, and
multi-row `values (..),(..)` must be handled. It needs the DSL-style
per-position restructure (a `DynamicSubgrammar` emitting one
boundary-aware position per column), tracked as its own step.
**Known limitation (both phases, matches the DSL).** `date` / `shortid` /
`datetime` **format** is still not validated at parse — those slots accept
any quoted string; the format is checked at bind/execution time (Phase 2).
So the live highlight catches *numeric-shape* mismatches (`int`/`decimal`/
`bool`), not malformed dates. The column-type **hint** still shows for
every type.
## See also
- ADR-0030 §4 / ADR-0033 §10 — the execute-path this ADR **augments**
+1 -1
View File
File diff suppressed because one or more lines are too long