sqlx-record/.claude/skills/sqlx-soft-delete.md

242 lines
5.3 KiB
Markdown

# sqlx-record Soft Delete Skill
Guide to soft delete functionality with #[soft_delete] attribute.
## Triggers
- "soft delete", "soft-delete"
- "is_deleted", "deleted"
- "restore", "undelete"
- "hard delete", "permanent delete"
## Overview
Soft delete allows marking records as deleted without removing them from the database. This enables:
- Recovery of accidentally deleted data
- Audit trails of deletions
- Referential integrity preservation
## Enabling Soft Delete
Add `#[soft_delete]` to a boolean field:
```rust
use sqlx_record::prelude::*;
#[derive(Entity, FromRow)]
#[table_name = "users"]
struct User {
#[primary_key]
id: Uuid,
name: String,
#[soft_delete]
is_deleted: bool, // Must be bool type
}
```
Auto-detection: Fields named `is_deleted` or `deleted` with `bool` type are automatically treated as soft delete fields even without the attribute.
## Generated Methods
### delete() / delete_by_{pk}()
Sets the soft delete field to `true`:
```rust
// Instance method
user.delete(&pool).await?;
// Static method by primary key
User::delete_by_id(&pool, &user_id).await?;
```
**SQL generated:**
```sql
UPDATE users SET is_deleted = TRUE WHERE id = ?
```
### hard_delete() / hard_delete_by_{pk}()
Permanently removes the row:
```rust
// Instance method
user.hard_delete(&pool).await?;
// Static method by primary key
User::hard_delete_by_id(&pool, &user_id).await?;
```
**SQL generated:**
```sql
DELETE FROM users WHERE id = ?
```
### restore() / restore_by_{pk}()
Sets the soft delete field to `false`:
```rust
// Instance method
user.restore(&pool).await?;
// Static method by primary key
User::restore_by_id(&pool, &user_id).await?;
```
**SQL generated:**
```sql
UPDATE users SET is_deleted = FALSE WHERE id = ?
```
### soft_delete_field()
Returns the field name:
```rust
let field = User::soft_delete_field(); // "is_deleted"
```
## Filtering Deleted Records
Soft delete does **NOT** automatically filter `find()` queries. You must add the filter manually:
```rust
// Include only non-deleted
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
// Include only deleted (trash view)
let deleted = User::find(&pool, filters![("is_deleted", true)], None).await?;
// Include all records
let all = User::find(&pool, filters![], None).await?;
```
### Helper Pattern
Create a helper function for consistent filtering:
```rust
impl User {
pub async fn find_active(
pool: &Pool,
mut filters: Vec<Filter<'_>>,
index: Option<&str>
) -> Result<Vec<Self>, sqlx::Error> {
filters.push(Filter::Equal("is_deleted", false.into()));
Self::find(pool, filters, index).await
}
}
// Usage
let users = User::find_active(&pool, filters![("role", "admin")], None).await?;
```
## Usage Examples
### Basic Soft Delete Flow
```rust
// Create user
let user = User {
id: new_uuid(),
name: "Alice".into(),
is_deleted: false,
};
user.insert(&pool).await?;
// Soft delete
user.delete(&pool).await?;
// user still exists in DB with is_deleted = true
// Find won't return deleted users (with proper filter)
let users = User::find(&pool, filters![("is_deleted", false)], None).await?;
// Alice not in results
// Restore
User::restore_by_id(&pool, &user.id).await?;
// user.is_deleted = false again
// Hard delete (permanent)
User::hard_delete_by_id(&pool, &user.id).await?;
// Row completely removed from database
```
### With Audit Trail
```rust
use sqlx_record::{transaction, prelude::*};
async fn soft_delete_with_audit(
pool: &Pool,
user_id: &Uuid,
actor_id: &Uuid
) -> Result<(), sqlx::Error> {
transaction!(&pool, |tx| {
// Soft delete the user
User::delete_by_id(&mut *tx, user_id).await?;
// Record the deletion
let change = EntityChange {
id: new_uuid(),
entity_id: *user_id,
action: "delete".into(),
changed_at: chrono::Utc::now().timestamp_millis(),
actor_id: *actor_id,
session_id: Uuid::nil(),
change_set_id: Uuid::nil(),
new_value: None,
};
create_entity_change(&mut *tx, "entity_changes_users", &change).await?;
Ok::<_, sqlx::Error>(())
}).await
}
```
### Cascade Soft Delete
```rust
async fn delete_user_cascade(pool: &Pool, user_id: &Uuid) -> Result<(), sqlx::Error> {
transaction!(&pool, |tx| {
// Soft delete user's orders
let orders = Order::find(&mut *tx, filters![("user_id", user_id)], None).await?;
for order in orders {
order.delete(&mut *tx).await?;
}
// Soft delete user
User::delete_by_id(&mut *tx, user_id).await?;
Ok::<_, sqlx::Error>(())
}).await
}
```
## Database Schema
Recommended column definition:
```sql
-- MySQL
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
-- PostgreSQL
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
-- SQLite
is_deleted INTEGER NOT NULL DEFAULT 0 -- 0=false, 1=true
```
Add an index for efficient filtering:
```sql
CREATE INDEX idx_users_is_deleted ON users (is_deleted);
-- Or composite index for common queries
CREATE INDEX idx_users_active_name ON users (is_deleted, name);
```
## Notes
- Soft delete field must be `bool` type
- The field is included in UpdateForm (can be manually toggled)
- Consider adding `deleted_at: Option<i64>` for deletion timestamps
- For complex filtering, consider database views