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:
+42
@@ -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:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user