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:
@@ -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 1–2 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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user