docs: ADR-0036 (Proposed) — bind literal DML values, verbatim text only for expressions/queries

Records the decision that advanced-mode SQL DML should stop handing literal
data values to the engine as text and instead parse/validate/bind them
through the DSL's proven path — closing the value-validation gap, the
hint/highlight gap, and the offending-value-in-errors gap together. Verbatim
text stays for expressions, WHERE, INSERT…SELECT, and SELECT (full SQL
surface preserved; ADR-0026's limited Expr not imposed). Narrows ADR-0030 §4
/ ADR-0033 §10 once accepted; SELECT half of §4 stands.

Includes a characterization test (tests/sql_insert.rs) proving the bind-layer
gap: the DSL rejects the malformed date 2025/01/15, advanced-mode SQL accepts
it. Forward-notes added to ADR-0030/0033; README index updated.

Status: Proposed (design + /runda done; pending go-ahead to implement).
This commit is contained in:
claude@clouddev1
2026-05-26 20:49:40 +00:00
parent f8a91f41c9
commit 3e3a2fb171
5 changed files with 403 additions and 1 deletions
+54 -1
View File
@@ -21,9 +21,11 @@
//! real reconstructed SQL.
use rdbms_playground::db::{Database, DbError, InsertResult};
use rdbms_playground::dsl::{ColumnSpec, Command, Type, parse_command};
use rdbms_playground::dsl::{ColumnSpec, Command, Type, Value, parse_command};
use rdbms_playground::event::AppEvent;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;
use rdbms_playground::runtime::run_replay;
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
@@ -1033,3 +1035,54 @@ fn autofill_preserves_on_conflict_clause() {
let rows = csv_rows(&project, "t");
assert_eq!(rows.len(), 1, "one row landed");
}
#[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.
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(id int pk, d date)");
// DSL path — validates the `date` and rejects the malformed value.
let dsl = r.block_on(db.insert(
"T".to_string(),
Some(vec!["id".to_string(), "d".to_string()]),
vec![Value::Number("1".to_string()), Value::Text("2025/01/15".to_string())],
Some("insert".to_string()),
));
assert!(
dsl.is_err(),
"the DSL insert path validates `date` and rejects 2025/01/15; got {dsl:?}"
);
// SQL path (advanced mode, full pipeline) — currently ACCEPTS it.
std::fs::write(
project.path().join("ins.commands"),
"insert into T (id, d) values (2, '2025/01/15')\n",
)
.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:?}"
);
}