Soft Deletion Architecture
Date: October 2025
Status: Production-ready
Branch: feature/soft-deletion-for-entities
Overview
This document describes the soft deletion implementation in the Rhesis backend. Soft deletion allows records to be marked as deleted without physically removing them from the database, enabling data recovery, maintaining referential integrity, and preserving historical data.
Design Goals
The soft deletion implementation was designed to achieve the following objectives:
1. Seamless Implementation Across All Entities
Leverage the Base model to provide soft deletion capabilities to all 31+ database entities automatically, without requiring changes to individual model definitions. This ensures consistency and reduces maintenance overhead.
2. Automatic Filtering at the Fundamental Level
Implement filtering that works automatically for ALL queries throughout the codebase without requiring manual modifications. This was critical because changing every existing query would be impractical and error-prone in a large codebase.
3. Preserve Referential Integrity and Historical Data
In a multi-entity database with complex foreign key relationships, cascade deletion would destroy valuable historical data. For example, tests should be deletable without losing test run history and results. Soft deletion preserves all relationships while hiding the deleted entity from normal operations.
4. Support Correct Pagination
Ensure soft delete filters are applied BEFORE LIMIT/OFFSET in SQL queries, so pagination counts and results are accurate. This required special handling of SQLAlchemy’s query compilation order.
5. Provide Flexible Control Mechanisms
Offer multiple layers of control: global context manager for admin operations, query-level methods for specific cases, and automatic filtering for normal operations. This balance supports different use cases without compromising security.
Architecture
The implementation uses a layered approach with four complementary mechanisms:
1. Base Model Enhancement
All models inherit from Base which now includes:
Location: apps/backend/src/rhesis/backend/app/models/base.py
2. SQLAlchemy Event Listener (Automatic Filtering)
A before_compile event listener automatically adds deleted_at IS NULL filter to ALL queries. This is the core mechanism that enables automatic filtering without code changes.
Key features:
- Intercepts queries at compilation time
- Catches
InvalidRequestErrorfor queries withLIMIT/OFFSET - Modifies
_where_criteriatuple directly when.filter()fails - Respects
_include_soft_deletedflag for explicit control
Location: apps/backend/src/rhesis/backend/app/models/soft_delete_events.py
3. QueryBuilder Enhancements
New methods for explicit control over soft delete behavior:
Location: apps/backend/src/rhesis/backend/app/utils/model_utils.py
4. Context Manager (Global Control)
Use the without_soft_delete_filter() context manager to temporarily disable filtering:
Location: apps/backend.src/rhesis/backend/app/database.py
CRUD Operations
The CRUD utilities have been enhanced to support soft deletion:
Location: apps/backend/src/rhesis/backend/app/utils/crud_utils.py
Recycle Bin API (Superuser Only)
A complete REST API for managing deleted records is available at /recycle:
List Available Models
Get Deleted Records
Restore a Record
Permanently Delete a Record
Get Recycle Bin Statistics
Bulk Restore
Empty Recycle Bin for a Model
Location: apps/backend/src/rhesis/backend/app/routers/recycle.py
How Pagination Works with Soft Deletion
This is a critical aspect of the implementation:
The event listener ensures soft delete filters are applied BEFORE LIMIT/OFFSET in the SQL query. Here’s how:
- When
.first()or.limit()are called, SQLAlchemy addsLIMITbefore query compilation - The event listener attempts to use
.filter()to add the soft delete condition - If
InvalidRequestErroris raised (becauseLIMIT/OFFSETalready applied), the listener catches it - It then modifies the
_where_criteriatuple directly, ensuring the filter becomes part of theWHEREclause
Result: Both count queries and paginated results correctly exclude deleted records, providing accurate pagination metadata.
Example Query Behavior
Database Migration
The migration adds deleted_at column and index to all tables:
Migration file: apps/backend/src/rhesis/backend/alembic/versions/e364aaec703f_add_soft_delete_support.py
Testing
Comprehensive test suite with 35 passing tests covering:
- ✅ CRUD operations (soft delete, restore, hard delete)
- ✅ QueryBuilder methods (
with_deleted,only_deleted) - ✅ Event listener automatic filtering
- ✅ Context manager behavior
- ✅ Edge cases and pagination scenarios
- ✅ Recycle bin API endpoints
- ✅ Multi-organization filtering
- ✅ Raw query filtering (including
.first())
Test files:
tests/backend/utils/test_soft_delete_crud.pytests/backend/utils/test_soft_delete_querybuilder.pytests/backend/routes/test_recycle.py
Usage Examples
Basic Soft Delete and Restore
Using QueryBuilder
Using Context Manager
Key Implementation Files
| File | Purpose |
|---|---|
app/models/base.py | Base model with soft deletion columns and methods |
app/models/soft_delete_events.py | SQLAlchemy event listener for automatic filtering |
app/database.py | Context manager for global control |
app/utils/crud_utils.py | Enhanced CRUD operations |
app/utils/model_utils.py | QueryBuilder with soft delete methods |
app/routers/recycle.py | REST API for recycle bin management |
alembic/versions/e364aaec703f_add_soft_delete_support.py | Database migration |
Summary
This soft deletion implementation provides a robust, production-ready solution that:
✅ Automatically filters ALL queries (including raw db.query().first() calls)
✅ Works correctly with pagination and LIMIT/OFFSET queries
✅ Preserves referential integrity and historical data
✅ Provides flexible control at multiple levels (context, query, method)
✅ Includes superuser recycle bin for data recovery
✅ Zero breaking changes to existing code
Related Documentation
Support
For questions or issues related to soft deletion, please contact the backend team or file an issue on GitHub.