feat: ADR-0036 Phase 1 — validate advanced-mode INSERT literals + show the value
Capture literal VALUES at parse onto Command::SqlInsert (no grammar change, no reparse); validate them against column types before the still-verbatim insert (reusing impl_value_for for DSL-parity wording); read them in the error enricher so a constraint error names the real value. Execution, auto-fill, and command identity unchanged. Adds run_sql_insert_with_literals (runtime path); run_sql_insert stays the no-capture raw entry. Proven: malformed date 2025/01/15 now refused in advanced-mode SQL; replayed UNIQUE shows the real value. Tests +3 (expression runs, multi-row, natural order) + 2 flipped/strengthened. 1930 pass / 0 fail / 0 skip; clippy clean.
This commit is contained in:
+111
-15
@@ -445,6 +445,7 @@ fn run_sqlinsert(
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
..
|
||||
} => rt.block_on(db.run_sql_insert(
|
||||
sql,
|
||||
Some(input.to_string()),
|
||||
@@ -1037,17 +1038,13 @@ fn autofill_preserves_on_conflict_clause() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_dml_skips_app_level_value_validation_that_the_dsl_enforces() {
|
||||
// CHARACTERIZATION of a real divergence (ADR pending — see the
|
||||
// verbatim-vs-structural DML discussion). The DSL insert path validates
|
||||
// a value against the playground type system at *bind* time
|
||||
// (`Value::bind_for_column` → `validate_date`); the verbatim SQL insert
|
||||
// path hands the literal straight to the engine, whose STRICT `date`
|
||||
// column is TEXT and accepts any string. `2025/01/15` is a malformed
|
||||
// date (slashes, not dashes): the DSL rejects it, advanced-mode SQL
|
||||
// currently accepts it. When the forthcoming ADR routes SQL literal
|
||||
// values through the same validation, FLIP the SQL assertion to expect
|
||||
// a rejection.
|
||||
fn sql_dml_validates_literal_values_like_the_dsl() {
|
||||
// ADR-0036 Phase 1: advanced-mode SQL `INSERT` now validates each
|
||||
// literal value against its column type before the (still verbatim)
|
||||
// insert runs, sharing the DSL's per-type validators. `2025/01/15` is
|
||||
// a malformed date (slashes, not dashes) — the DSL rejects it at bind
|
||||
// time, and advanced-mode SQL now refuses it too (it used to splice the
|
||||
// literal into text and let a STRICT TEXT column accept anything).
|
||||
let (project, db, _d) = open_project_db();
|
||||
let r = rt();
|
||||
r.block_on(db.create_table(
|
||||
@@ -1073,7 +1070,7 @@ fn sql_dml_skips_app_level_value_validation_that_the_dsl_enforces() {
|
||||
"the DSL insert path validates `date` and rejects 2025/01/15; got {dsl:?}"
|
||||
);
|
||||
|
||||
// SQL path (advanced mode, full pipeline) — currently ACCEPTS it.
|
||||
// SQL path (advanced mode, full pipeline) — now REJECTS it too.
|
||||
std::fs::write(
|
||||
project.path().join("ins.commands"),
|
||||
"insert into T (id, d) values (2, '2025/01/15')\n",
|
||||
@@ -1081,8 +1078,107 @@ fn sql_dml_skips_app_level_value_validation_that_the_dsl_enforces() {
|
||||
.expect("write script");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "ins.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count, .. }) if *count == 1),
|
||||
"CHARACTERIZATION: verbatim SQL insert skips `date` validation and \
|
||||
accepts 2025/01/15 — the gap the ADR will close; events: {events:?}"
|
||||
matches!(events.last(), Some(AppEvent::ReplayFailed { .. })),
|
||||
"advanced-mode SQL validates the `date` literal and refuses \
|
||||
2025/01/15 (ADR-0036 Phase 1); events: {events:?}"
|
||||
);
|
||||
// A valid date still inserts (the bound/verbatim path is unaffected).
|
||||
std::fs::write(
|
||||
project.path().join("ok.commands"),
|
||||
"insert into T (id, d) values (3, '2025-01-15')\n",
|
||||
)
|
||||
.expect("write script");
|
||||
let ok = r.block_on(run_replay(&db, project.path(), "ok.commands"));
|
||||
assert!(
|
||||
matches!(ok.last(), Some(AppEvent::ReplayCompleted { count, .. }) if *count == 1),
|
||||
"a well-formed date still inserts; events: {ok:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_insert_expression_value_is_not_validated_and_runs() {
|
||||
// An expression position (not a bare literal) is left to the engine —
|
||||
// ADR-0036 Phase 1 has nothing static to validate, so `1 + 2` into an
|
||||
// int column computes 3 and inserts; it must not be mis-classified as
|
||||
// a literal or rejected.
|
||||
let (project, db, _d) = open_project_db();
|
||||
let r = rt();
|
||||
r.block_on(db.create_table(
|
||||
"T".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Int),
|
||||
ColumnSpec::new("n", Type::Int),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
Some("create table T with pk id(int)".to_string()),
|
||||
))
|
||||
.expect("create T");
|
||||
std::fs::write(
|
||||
project.path().join("e.commands"),
|
||||
"insert into T (id, n) values (1, 1 + 2)\n",
|
||||
)
|
||||
.expect("write script");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "e.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayCompleted { count, .. }) if *count == 1),
|
||||
"the expression value executes (engine computes it); events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_insert_multi_row_validates_each_literal() {
|
||||
// Validation applies to every literal row; a malformed `date` in the
|
||||
// second tuple is caught (ADR-0036 Phase 1 — execution is verbatim, so
|
||||
// multi-row comes for free).
|
||||
let (project, db, _d) = open_project_db();
|
||||
let r = rt();
|
||||
r.block_on(db.create_table(
|
||||
"T".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Int),
|
||||
ColumnSpec::new("d", Type::Date),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
Some("create table T with pk id(int)".to_string()),
|
||||
))
|
||||
.expect("create T");
|
||||
std::fs::write(
|
||||
project.path().join("m.commands"),
|
||||
"insert into T (id, d) values (1, '2025-01-15'), (2, '2025/02/20')\n",
|
||||
)
|
||||
.expect("write script");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "m.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayFailed { .. })),
|
||||
"the malformed date in the second row is caught; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_insert_natural_order_validates_against_schema_columns() {
|
||||
// With no explicit column list, positions map to the schema's columns
|
||||
// in definition order (engine semantics) — validation must use that
|
||||
// mapping, not an explicit list (ADR-0036 Phase 1).
|
||||
let (project, db, _d) = open_project_db();
|
||||
let r = rt();
|
||||
r.block_on(db.create_table(
|
||||
"T".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Int),
|
||||
ColumnSpec::new("d", Type::Date),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
Some("create table T with pk id(int)".to_string()),
|
||||
))
|
||||
.expect("create T");
|
||||
std::fs::write(
|
||||
project.path().join("nat.commands"),
|
||||
"insert into T values (1, '2025/02/20')\n",
|
||||
)
|
||||
.expect("write script");
|
||||
let events = r.block_on(run_replay(&db, project.path(), "nat.commands"));
|
||||
assert!(
|
||||
matches!(events.last(), Some(AppEvent::ReplayFailed { .. })),
|
||||
"natural-order insert validates the date against column `d`; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user