---
url: https://docs.rhesis.ai/contribute/backend/cascade-operations
title: Cascade Deletion and Restoration
---
# 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](/contribute/backend/soft-deletion) for the underlying mechanics.

## Configuration

Each edge is a `CascadeRelationship` registered under its parent model in `CASCADE_RELATIONSHIPS`:

```python
@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 module (`crud/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.

## Related

- [Soft Deletion](/contribute/backend/soft-deletion)
- [Database Models](/contribute/backend/database-models)
- [API Structure](/contribute/backend/api-structure)