db: fix self-referential cascade over-count + SQL-delete render test

A self-referential ON DELETE CASCADE FK (e.g. T.ParentId -> T.id) is
returned by read_relationships_inbound as a child whose table IS the
delete target. The before/after row-count diff then includes the
directly-deleted rows (already in rows_affected), so deleting a chain
root reported 3 cascaded rows when only 2 were removed via the
self-reference.

Fix in both do_delete (DSL) and do_sql_delete (SQL): when the child
table equals the target, subtract rows_affected from the diff and
guard on the corrected count (a leaf delete no longer reports a
phantom 0-row self-cascade); the target's CSV is already queued, so a
self-ref child is not re-added to rewritten_tables. Pre-existing in
do_delete; surfaced by the 3f DA pass, fixed in both paths to keep
DSL/SQL parity. Behaviour: report only the rows removed via the
self-reference (user-confirmed).

Also adds an app-level render test for the SQL DELETE path
(handle_dsl_delete_success via CommandOutcome::Delete) — the shared
renderer's ok-summary + per-relationship cascade line were exercised
only through the DSL path before.

Test-first: self_referential_cascade_counts_only_cascaded_rows added
for both paths (asserted 2, failed at 3 before the fix). 1545 pass /
0 fail / 1 ignored. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-22 19:17:43 +00:00
parent 2c86a1313e
commit 62f09bebc5
3 changed files with 182 additions and 8 deletions
+42
View File
@@ -3413,4 +3413,46 @@ mod tests {
"header row rendered: {texts:?}",
);
}
#[test]
fn sql_delete_success_renders_count_and_cascade_summary() {
// ADR-0033 sub-phase 3f: a SQL DELETE reuses the DSL delete
// renderer (CommandOutcome::Delete -> handle_dsl_delete_
// success). This pins that the SHARED renderer produces the
// right user-facing strings for the SQL path — the ok-summary
// (verb + subject, where SqlDelete's subject is its target
// table) and the per-relationship cascade line. The integration
// tests check the DeleteResult struct; this checks the render.
use crate::dsl::ReferentialAction;
let mut app = App::new();
app.update(AppEvent::DslDeleteSucceeded {
command: Command::SqlDelete {
sql: "delete from Customers where id = 1".to_string(),
target_table: "Customers".to_string(),
},
result: crate::db::DeleteResult {
rows_affected: 1,
cascade: vec![crate::db::CascadeEffect {
relationship_name: "places".to_string(),
child_table: "Orders".to_string(),
rows_changed: 2,
action: ReferentialAction::Cascade,
}],
},
});
let texts: Vec<String> = app.output.iter().map(|l| l.text.clone()).collect();
assert!(
texts.iter().any(|t| t.contains("delete from") && t.contains("Customers")),
"ok summary names the verb + target table: {texts:?}",
);
assert!(
texts.iter().any(|t| t.contains("1 row(s) deleted")),
"directly-deleted count surfaced: {texts:?}",
);
assert!(
texts.iter().any(|t| t.contains("2 row(s) deleted in `Orders`")
&& t.contains("relationship `places`")),
"per-relationship cascade summary surfaced: {texts:?}",
);
}
}