grammar+db: 3e — SQL UPDATE grammar + execution (ADR-0033 §2)
New src/dsl/grammar/sql_update.rs: SQL_UPDATE_SHAPE =
<table> SET col = sql_expr (',' …)* [WHERE sql_expr] [';'], the
__rdbms_* target rejection, and the shared sql_expr on both the
assignment RHS and the predicate. No --all-rows rail — a SQL
UPDATE without WHERE runs as written (ADR-0030 §12). Reuses
sql_select::WHERE_CLAUSE (now pub(crate)) so the predicate
diagnostics are identical. The target uses the shared `table_name`
ident role (not a bespoke one) so the Phase-2 schema-existence and
predicate-warning passes collect it as a scope binding and check
the SET / WHERE columns for free — a bespoke role left them
unchecked (the cross-cut tests caught this).
Command::SqlUpdate { sql, target_table }; Request::RunSqlUpdate +
do_sql_update (execute validated SQL via execute_with_fk_enrichment,
re-persist the target CSV, append history.log). 3e surfaces the
affected-row count only; precise row output is RETURNING (3g), so
the update-success render skips a column-less data set rather than
showing a misleading "(no rows)" band. Behind the dev `sql_update`
entry word until 3j.
Tests: grammar accept/reject; integration (single/multi-col,
no-WHERE all-rows, sql_expr in SET, scalar subquery in SET,
zero-match success, history); walker cross-cut (unknown SET column
→ unknown_column, `= NULL` in WHERE → eq_null warning); app-level
render-guard both ways (column-less → count only; with columns →
table renders). 1524 green, clippy clean.
This commit is contained in:
@@ -595,6 +595,16 @@ enum Request {
|
||||
row_source: String,
|
||||
reply: oneshot::Sender<Result<InsertResult, DbError>>,
|
||||
},
|
||||
/// Run a grammar-validated SQL `UPDATE` (ADR-0033 §2). The
|
||||
/// worker executes `sql` as text, re-persists `target_table`'s
|
||||
/// CSV (ADR-0030 §11), and appends the literal line to
|
||||
/// `history.log`.
|
||||
RunSqlUpdate {
|
||||
sql: String,
|
||||
source: Option<String>,
|
||||
target_table: String,
|
||||
reply: oneshot::Sender<Result<UpdateResult, DbError>>,
|
||||
},
|
||||
/// Capture the query plan for an explainable command via
|
||||
/// `EXPLAIN QUERY PLAN` (ADR-0028 §2). `query` is the inner
|
||||
/// `ShowData` / `Update` / `Delete`; `EXPLAIN QUERY PLAN`
|
||||
@@ -1078,6 +1088,28 @@ 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
|
||||
/// grammar-validated statement text; `source` is the literal
|
||||
/// 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,
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::RunSqlUpdate {
|
||||
sql,
|
||||
source,
|
||||
target_table,
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
recv.await.map_err(|_| DbError::WorkerGone)?
|
||||
}
|
||||
|
||||
/// Capture the query plan for an explainable command
|
||||
/// (ADR-0028 §2). The wrapped command is not executed —
|
||||
/// `EXPLAIN QUERY PLAN` only inspects how the engine would
|
||||
@@ -1541,6 +1573,20 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
|
||||
&row_source,
|
||||
));
|
||||
}
|
||||
Request::RunSqlUpdate {
|
||||
sql,
|
||||
source,
|
||||
target_table,
|
||||
reply,
|
||||
} => {
|
||||
let _ = reply.send(do_sql_update(
|
||||
conn,
|
||||
persistence,
|
||||
source.as_deref(),
|
||||
&sql,
|
||||
&target_table,
|
||||
));
|
||||
}
|
||||
Request::RebuildFromText {
|
||||
project_path,
|
||||
source,
|
||||
@@ -5975,6 +6021,54 @@ fn do_sql_insert(
|
||||
})
|
||||
}
|
||||
|
||||
/// Worker handler for `Request::RunSqlUpdate` (ADR-0033 §2,
|
||||
/// sub-phase 3e). Mirrors `do_sql_insert`'s persistence
|
||||
/// discipline: run the validated SQL inside a transaction,
|
||||
/// re-persist the target table's CSV + append `history.log` via
|
||||
/// `finalize_persistence` *before* `tx.commit()`, then commit.
|
||||
///
|
||||
/// Grammar-as-text (ADR-0030 §4): the assignment and predicate
|
||||
/// values are literals in `sql`, so no parameters are bound. A SQL
|
||||
/// `UPDATE` without `WHERE` runs across all rows as written
|
||||
/// (ADR-0030 §12 — no `--all-rows` rail). An update matching zero
|
||||
/// rows is a success (`rows_affected == 0`); the persistence
|
||||
/// write-through still runs (re-persisting the unchanged CSV is a
|
||||
/// no-op-equivalent and keeps the path uniform).
|
||||
///
|
||||
/// Auto-show: 3e returns an empty [`DataResult`] — the affected
|
||||
/// rows can't be shown precisely without `RETURNING` (sub-phase
|
||||
/// 3g, which is the precise tool). The summary surfaces the
|
||||
/// affected-row count; the renderer skips the (column-less) table.
|
||||
fn do_sql_update(
|
||||
conn: &Connection,
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
sql: &str,
|
||||
target_table: &str,
|
||||
) -> Result<UpdateResult, DbError> {
|
||||
debug!(sql = %sql, table = %target_table, "sql_update");
|
||||
let tx = conn
|
||||
.unchecked_transaction()
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
let rows_affected = execute_with_fk_enrichment(conn, target_table, sql, &[])?;
|
||||
let changes = Changes {
|
||||
schema_dirty: false,
|
||||
rewritten_tables: vec![target_table.to_string()],
|
||||
..Changes::default()
|
||||
};
|
||||
finalize_persistence(conn, persistence, source, &changes)?;
|
||||
tx.commit().map_err(DbError::from_rusqlite)?;
|
||||
Ok(UpdateResult {
|
||||
rows_affected,
|
||||
data: DataResult {
|
||||
table_name: target_table.to_string(),
|
||||
columns: Vec::new(),
|
||||
column_types: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a grammar-validated SQL `SELECT` and collect its
|
||||
/// rows into a [`DataResult`] (ADR-0030 §6, ADR-0032 §12 +
|
||||
/// Amendment 1).
|
||||
|
||||
Reference in New Issue
Block a user