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
+35
View File
@@ -20,9 +20,12 @@
//! worker-level tests call `db.run_sql_insert` directly with the
//! real reconstructed SQL.
use rdbms_playground::completion::{SchemaCache, TableColumn};
use rdbms_playground::db::{Database, DbError, InsertResult};
use rdbms_playground::dsl::{ColumnSpec, Command, Type, Value, parse_command};
use rdbms_playground::event::AppEvent;
use rdbms_playground::input_render::{AmbientHint, ambient_hint_in_mode};
use rdbms_playground::mode::Mode;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;
use rdbms_playground::runtime::run_replay;
@@ -1182,3 +1185,35 @@ fn sql_insert_natural_order_validates_against_schema_columns() {
"natural-order insert validates the date against column `d`; events: {events:?}"
);
}
// =================================================================
// ADR-0036 Phase 3a — live typed-slot hint for the UPSERT
// `ON CONFLICT … DO UPDATE SET col = <rhs>` value position.
// =================================================================
#[test]
fn advanced_upsert_do_update_set_offers_typed_slot_hint() {
// ADR-0036 Phase 3a: the `DO UPDATE SET col = ` value position
// shares the SQL UPDATE `SET` treatment, so it drives the same
// column-typed slot hint (boundary-aware lookahead → typed slot).
let mut cache = SchemaCache::default();
let cols = vec![
TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false },
TableColumn { name: "Name".to_string(), user_type: Type::Text, not_null: false, has_default: false },
];
cache.tables.push("Customers".to_string());
cache.columns.push("id".to_string());
cache.columns.push("Name".to_string());
cache.table_columns.insert("Customers".to_string(), cols);
let input = "insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name=";
let hint = ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced);
let Some(AmbientHint::Prose(prose)) = hint else {
panic!("expected a Prose hint at the UPSERT SET value slot, got {hint:?}");
};
assert!(prose.contains("Name"), "hint names the column `Name`: {prose:?}");
assert!(
prose.contains("quoted string"),
"text-column hint says `quoted string`: {prose:?}"
);
}