feat: ADR-0036 Phase 2 — validate advanced-mode UPDATE SET literals + retain the value

Mirror Phase 1's capture-at-parse technique on the UPDATE SET assignment
list. build_sql_update calls the new capture_set_literals (data.rs), which
walks the matched tokens (no reparse, no grammar change) and classifies
each top-level `SET col = <rhs>` as a literal (Some, incl. signed numbers)
or an expression (None), using paren depth so a comma inside a function
call or a `where` inside a scalar subquery is not mistaken for a boundary,
and the trailing top-level WHERE is excluded.

Command::SqlUpdate gains set_literals; do_sql_update validates the literals
against their column types via the shared impl_value_for before the still
verbatim update; user_value_for_column reads them so a constraint error
names the offending value. WHERE stays unvalidated; execution and command
identity are unchanged.

Also corrects the stale data.rs header comment (DSL typed slots are wired,
not "deferred") and flips ADR-0036 + README to Phases 1–2 implemented.

Tests: 1934 passing (+4), 0 failed, 0 skipped, 1 ignored; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-26 22:20:12 +00:00
parent 2f0af31b3b
commit 8c3b13b313
10 changed files with 413 additions and 27 deletions
+70
View File
@@ -200,6 +200,76 @@ fn enrich_unique_update_resolves_value_from_assignments() {
});
}
#[test]
fn enrich_unique_sql_update_resolves_value_from_set_literals() {
// ADR-0036 Phase 2: an advanced-mode SQL `UPDATE` now retains its
// `SET` literals, so a UNIQUE violation names the offending value —
// closing the error-value gap for advanced mode, mirroring the DSL
// `Update` case above. The value flows from the parse-captured
// `set_literals` through `user_value_for_column`.
let db = db();
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec::new("id".to_string(), Type::Int),
ColumnSpec::new("name".to_string(), Type::Text),
],
vec!["id".to_string()],
None,
)
.await
.unwrap();
db.insert(
"Customers".to_string(),
None,
vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())],
None,
)
.await
.unwrap();
db.insert(
"Customers".to_string(),
None,
vec![Value::Number("2".to_string()), Value::Text("Bob".to_string())],
None,
)
.await
.unwrap();
// Advanced-mode SQL: set Bob's id to 1 — collides with Alice.
let input = "update Customers set id = 1 where name = 'Bob'";
let cmd = parse_command(input).expect("parses as advanced-mode SQL update");
let Command::SqlUpdate {
sql,
target_table,
returning,
set_literals,
} = cmd.clone()
else {
panic!("expected Command::SqlUpdate, got {cmd:?}");
};
// The literal `1` is a valid int, so Phase-2 validation passes and
// the engine-level UNIQUE violation is what surfaces.
let err = db
.run_sql_update_with_literals(sql, None, target_table, returning, set_literals)
.await
.unwrap_err();
assert!(matches!(
err,
DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. }
));
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
assert_eq!(facts.column.as_deref(), Some("id"));
assert_eq!(
facts.value.as_deref(),
Some("1"),
"the offending SET value is named (from set_literals)"
);
});
}
// ---- NOT NULL ---------------------------------------------------
#[test]