Skip to Content
ContributeBackendCascade Operations

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:

cascade_config.py
@dataclass
class CascadeRelationship:
    child_model: Type              # child entity model
    foreign_key: str               # FK column on the child pointing at the parent
    cascade_delete: bool = True
    cascade_restore: bool = True
    description: str = ""
    extra_filters: Dict[str, Any] = field(default_factory=dict)  # polymorphic filters

CASCADE_RELATIONSHIPS: Dict[Type, List[CascadeRelationship]] = {
    models.TestRun: [
        CascadeRelationship(models.TestResult, foreign_key="test_run_id"),
    ],
    # Polymorphic child (File keyed by entity_type) needs extra_filters
    models.Test: [
        CascadeRelationship(
            models.File, foreign_key="entity_id",
            extra_filters={"entity_type": "Test"},
        ),
    ],
}

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, the foreign_key matches the actual column, and cascade_delete is True.
  • 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 DELETE constraints where needed.