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:
@@ -711,6 +711,10 @@ enum Request {
|
||||
source: Option<String>,
|
||||
target_table: String,
|
||||
returning: bool,
|
||||
/// Captured literal `SET col = <literal>` values (`(col, None)` =
|
||||
/// expression RHS) for app-level type validation before the
|
||||
/// verbatim update (ADR-0036 Phase 2).
|
||||
set_literals: Vec<(String, Option<Value>)>,
|
||||
reply: oneshot::Sender<Result<UpdateResult, DbError>>,
|
||||
},
|
||||
/// Run a grammar-validated SQL `DELETE` (ADR-0033 §1/§7). The
|
||||
@@ -1506,17 +1510,38 @@ impl Database {
|
||||
recv.await.map_err(|_| DbError::WorkerGone)?
|
||||
}
|
||||
|
||||
/// Run a validated SQL `UPDATE` and return the affected-row
|
||||
/// count (ADR-0033 §2, sub-phase 3e). `sql` is the
|
||||
/// Run a validated SQL `UPDATE` 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_update_with_literals`] instead so the `SET`
|
||||
/// literals are validated (ADR-0036 Phase 2). `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.
|
||||
/// submitted line for `history.log`; `target_table` is the parsed
|
||||
/// target whose CSV is re-persisted.
|
||||
pub async fn run_sql_update(
|
||||
&self,
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
target_table: String,
|
||||
returning: bool,
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
self.run_sql_update_with_literals(sql, source, target_table, returning, Vec::new())
|
||||
.await
|
||||
}
|
||||
|
||||
/// As [`Self::run_sql_update`], plus the literal `SET` values captured
|
||||
/// at parse (`(col, None)` = expression RHS) so the worker can
|
||||
/// validate each literal against its column type before the (still
|
||||
/// verbatim) update and the error layer can name the offending value
|
||||
/// (ADR-0036 Phase 2).
|
||||
pub async fn run_sql_update_with_literals(
|
||||
&self,
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
target_table: String,
|
||||
returning: bool,
|
||||
set_literals: Vec<(String, Option<Value>)>,
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::RunSqlUpdate {
|
||||
@@ -1524,6 +1549,7 @@ impl Database {
|
||||
source,
|
||||
target_table,
|
||||
returning,
|
||||
set_literals,
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
@@ -2493,6 +2519,7 @@ fn handle_request(
|
||||
source,
|
||||
target_table,
|
||||
returning,
|
||||
set_literals,
|
||||
reply,
|
||||
} => {
|
||||
snapshot_then(snap, batch, conn, source.as_deref(), reply, || do_sql_update(
|
||||
@@ -2502,6 +2529,7 @@ fn handle_request(
|
||||
&sql,
|
||||
&target_table,
|
||||
returning,
|
||||
&set_literals,
|
||||
));
|
||||
}
|
||||
Request::RunSqlDelete {
|
||||
@@ -8545,10 +8573,27 @@ fn do_sql_update(
|
||||
sql: &str,
|
||||
target_table: &str,
|
||||
returning: bool,
|
||||
set_literals: &[(String, Option<Value>)],
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
debug!(sql = %sql, table = %target_table, returning, "sql_update");
|
||||
let canonical_table = require_canonical_table(conn, target_table)?;
|
||||
let target_table = canonical_table.as_str();
|
||||
|
||||
// ADR-0036 Phase 2: validate each captured `SET col = <literal>`
|
||||
// against its column type BEFORE the (still verbatim) update runs —
|
||||
// sharing the DSL's per-type validators (`impl_value_for`, the same
|
||||
// helper `build_update_sql` uses) for identical wording. Only literal
|
||||
// assignments are checked; expression positions (`None`) and the
|
||||
// `WHERE` predicate are left to the engine (ADR-0036 §2). Execution
|
||||
// below is unchanged (no binding).
|
||||
if set_literals.iter().any(|(_, v)| v.is_some()) {
|
||||
let schema = read_schema(conn, target_table)?;
|
||||
for (col, slot) in set_literals {
|
||||
if let Some(value) = slot {
|
||||
impl_value_for(&schema, col, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let tx = conn
|
||||
.unchecked_transaction()
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
|
||||
Reference in New Issue
Block a user