Cascade Deletion and Restoration
Soft-deleting or restoring a parent often needs the same operation on its children (deleting a TestRun should soft-delete its TestResult rows). Rather than repeating that logic in every CRUD helper, parent-child edges are declared once in config/cascade_config.py and applied automatically by delete_item / restore_item. See Soft Deletion for the underlying mechanics.
Configuration
Each edge is a CascadeRelationship registered under its parent model in CASCADE_RELATIONSHIPS:
Adding a relationship is config-only — no CRUD or service code changes. Set cascade_delete=False or cascade_restore=False to opt an edge out (e.g. preserve an audit trail on delete, or require manual review before restoring). Add a test alongside the config change.
How it works
delete_item and restore_item call the cascade service (services/cascade.py), which reads the config and issues a single bulk UPDATE per child relationship (deleted_at = now() on delete, deleted_at = NULL on restore) rather than loading objects. Child updates and the parent change commit together, so a failure rolls back everything.
Organization filtering is applied automatically when the child model has an organization_id, preventing cross-org cascades.
Cascade is one level deep. The service updates a parent’s immediate configured children only; it does not recurse. If you need Project → TestSet → Test to fully cascade, that chaining is not automatic — the bulk UPDATE on children does not re-trigger the children’s own cascades.
Troubleshooting
- Children not cascading — confirm the edge is in
CASCADE_RELATIONSHIPS, theforeign_keymatches the actual column, andcascade_deleteisTrue. - Slow cascades — ensure the child FK column is indexed; the service already uses bulk
UPDATE, so check for missing indexes rather than object loading. - Orphaned children after hard delete — soft-delete cascades before any hard delete; back them with database
ON DELETEconstraints where needed.