# 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>, index: Option<&str> ) -> Result, 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` for deletion timestamps - For complex filtering, consider database views