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:
@@ -696,6 +696,10 @@ enum Request {
|
||||
listed_columns: Vec<String>,
|
||||
row_source: String,
|
||||
returning: bool,
|
||||
/// Captured literal `VALUES` (per row, per position; `None` =
|
||||
/// expression) for app-level type validation before the verbatim
|
||||
/// insert (ADR-0036 Phase 1).
|
||||
literal_rows: Vec<Vec<Option<Value>>>,
|
||||
reply: oneshot::Sender<Result<InsertResult, DbError>>,
|
||||
},
|
||||
/// Run a grammar-validated SQL `UPDATE` (ADR-0033 §2). The
|
||||
@@ -1444,6 +1448,12 @@ impl Database {
|
||||
/// `sql` is the grammar-validated statement text; `source` is
|
||||
/// the literal submitted line for `history.log`; `target_table`
|
||||
/// is the parsed target whose CSV is re-persisted.
|
||||
/// Run a grammar-validated SQL `INSERT` with **no** captured literals
|
||||
/// (no app-level value validation — the verbatim ADR-0033 path). Used
|
||||
/// by worker-level callers that build the statement directly. The
|
||||
/// runtime, which has a parsed command, uses
|
||||
/// [`Self::run_sql_insert_with_literals`] instead so the literals are
|
||||
/// validated (ADR-0036 Phase 1).
|
||||
pub async fn run_sql_insert(
|
||||
&self,
|
||||
sql: String,
|
||||
@@ -1452,6 +1462,34 @@ impl Database {
|
||||
listed_columns: Vec<String>,
|
||||
row_source: String,
|
||||
returning: bool,
|
||||
) -> Result<InsertResult, DbError> {
|
||||
self.run_sql_insert_with_literals(
|
||||
sql,
|
||||
source,
|
||||
target_table,
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
Vec::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// As [`Self::run_sql_insert`], plus the literal `VALUES` captured at
|
||||
/// parse (per row, per position; `None` = expression) so the worker
|
||||
/// can validate each literal against its column type before the
|
||||
/// (still verbatim) insert and the error layer can name the offending
|
||||
/// value (ADR-0036 Phase 1).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn run_sql_insert_with_literals(
|
||||
&self,
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
target_table: String,
|
||||
listed_columns: Vec<String>,
|
||||
row_source: String,
|
||||
returning: bool,
|
||||
literal_rows: Vec<Vec<Option<Value>>>,
|
||||
) -> Result<InsertResult, DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::RunSqlInsert {
|
||||
@@ -1461,6 +1499,7 @@ impl Database {
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
literal_rows,
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
@@ -2434,6 +2473,7 @@ fn handle_request(
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
literal_rows,
|
||||
reply,
|
||||
} => {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_sql_insert(
|
||||
@@ -2445,6 +2485,7 @@ fn handle_request(
|
||||
&listed_columns,
|
||||
&row_source,
|
||||
returning,
|
||||
&literal_rows,
|
||||
));
|
||||
}
|
||||
Request::RunSqlUpdate {
|
||||
@@ -8388,10 +8429,38 @@ fn do_sql_insert(
|
||||
listed_columns: &[String],
|
||||
row_source: &str,
|
||||
returning: bool,
|
||||
literal_rows: &[Vec<Option<Value>>],
|
||||
) -> Result<InsertResult, DbError> {
|
||||
debug!(sql = %sql, table = %target_table, returning, "sql_insert");
|
||||
let canonical_table = require_canonical_table(conn, target_table)?;
|
||||
let target_table = canonical_table.as_str();
|
||||
|
||||
// ADR-0036 Phase 1: validate captured literal VALUES against their
|
||||
// column types BEFORE the (still verbatim) insert runs — sharing the
|
||||
// DSL's per-type validators (`impl_value_for`) for identical wording.
|
||||
// Only literal positions are checked; expression positions (`None`)
|
||||
// are left to the engine. Column mapping mirrors the engine's: an
|
||||
// explicit column list maps by position; natural order maps to the
|
||||
// schema's columns in definition order. An out-of-range position
|
||||
// (arity mismatch) is left for the engine / parse-time diagnostic.
|
||||
// Execution below is unchanged (no binding, no auto-fill change).
|
||||
if literal_rows.iter().any(|r| r.iter().any(Option::is_some)) {
|
||||
let schema = read_schema(conn, target_table)?;
|
||||
let columns: Vec<&str> = if listed_columns.is_empty() {
|
||||
schema.columns.iter().map(|c| c.name.as_str()).collect()
|
||||
} else {
|
||||
listed_columns.iter().map(String::as_str).collect()
|
||||
};
|
||||
for row in literal_rows {
|
||||
for (idx, slot) in row.iter().enumerate() {
|
||||
if let Some(value) = slot
|
||||
&& let Some(col) = columns.get(idx)
|
||||
{
|
||||
impl_value_for(&schema, col, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The `shortid` auto-fill rewrite reconstructs only `INSERT …
|
||||
// VALUES …` and would drop any trailing clause — `ON CONFLICT …`
|
||||
// (3h) and/or `RETURNING …` (3g). `row_source` is the clean
|
||||
|
||||
Reference in New Issue
Block a user