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:
+73
-2
@@ -1251,8 +1251,14 @@ impl App {
|
||||
fn handle_dsl_update_success(&mut self, command: &Command, result: &UpdateResult) {
|
||||
self.note_ok_summary(command);
|
||||
self.note_system(crate::t!("ok.rows_updated", count = result.rows_affected));
|
||||
for line in crate::output_render::render_data_table(&result.data) {
|
||||
self.note_system(line);
|
||||
// A column-less result carries no rows to tabulate (the SQL
|
||||
// UPDATE path before `RETURNING`, ADR-0033 sub-phase 3e):
|
||||
// surface just the count rather than a misleading
|
||||
// "(no rows)" band. The DSL UPDATE always has columns.
|
||||
if !result.data.columns.is_empty() {
|
||||
for line in crate::output_render::render_data_table(&result.data) {
|
||||
self.note_system(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1474,6 +1480,11 @@ impl App {
|
||||
C::SqlInsert { target_table, .. } => {
|
||||
(Operation::Insert, Some(target_table.as_str()), None)
|
||||
}
|
||||
// A SQL `UPDATE` (ADR-0033 §2) — route engine errors
|
||||
// through the update operation with the parsed target.
|
||||
C::SqlUpdate { target_table, .. } => {
|
||||
(Operation::Update, Some(target_table.as_str()), None)
|
||||
}
|
||||
C::Replay { .. } => (Operation::Replay, None, None),
|
||||
// An `explain` failure (e.g. unknown table) is best
|
||||
// described by the wrapped query it failed to plan.
|
||||
@@ -3336,4 +3347,64 @@ mod tests {
|
||||
Some(crate::dsl::walker::Severity::Warning),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_update_success_shows_count_without_no_rows_band() {
|
||||
// ADR-0033 sub-phase 3e: a SQL UPDATE returns a column-less
|
||||
// result (precise rows are RETURNING, 3g). The render must
|
||||
// surface the affected-row count and NOT a misleading
|
||||
// "(no rows)" table band.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslUpdateSucceeded {
|
||||
command: Command::SqlUpdate {
|
||||
sql: "update t set v = 1".to_string(),
|
||||
target_table: "t".to_string(),
|
||||
},
|
||||
result: crate::db::UpdateResult {
|
||||
rows_affected: 2,
|
||||
data: crate::db::DataResult {
|
||||
table_name: "t".to_string(),
|
||||
columns: Vec::new(),
|
||||
column_types: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
},
|
||||
},
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
texts.iter().any(|t| t.contains("2 row(s) updated")),
|
||||
"affected-row count surfaced: {texts:?}",
|
||||
);
|
||||
assert!(
|
||||
!texts.iter().any(|t| t.contains("(no rows)")),
|
||||
"no misleading empty-table band: {texts:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_success_with_columns_renders_the_table() {
|
||||
// The guard only suppresses a column-less result: a result
|
||||
// carrying columns (the DSL UPDATE path) still renders.
|
||||
let mut app = App::new();
|
||||
app.update(AppEvent::DslUpdateSucceeded {
|
||||
command: Command::SqlUpdate {
|
||||
sql: "update t set v = 1".to_string(),
|
||||
target_table: "t".to_string(),
|
||||
},
|
||||
result: crate::db::UpdateResult {
|
||||
rows_affected: 1,
|
||||
data: crate::db::DataResult {
|
||||
table_name: "t".to_string(),
|
||||
columns: vec!["id".to_string(), "v".to_string()],
|
||||
column_types: vec![Some(Type::Int), Some(Type::Int)],
|
||||
rows: vec![vec![Some("1".to_string()), Some("9".to_string())]],
|
||||
},
|
||||
},
|
||||
});
|
||||
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
|
||||
assert!(
|
||||
texts.iter().any(|t| t.contains("id") && t.contains('v')),
|
||||
"header row rendered: {texts:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user