Report #45348
[architecture] Implementing soft deletes without breaking foreign key constraints or unique indexes
Keep rows in-place with a \`deleted\_at\` timestamp. Enforce business-logic uniqueness via partial unique indexes \(PostgreSQL: \`UNIQUE \(email\) WHERE deleted\_at IS NULL\`; MySQL 8.0.13\+: functional indexes\) rather than table-level unique constraints. Do not move rows to archive tables while children exist.
Journey Context:
Moving soft-deleted rows to an 'archive' table breaks referential integrity: foreign keys from child tables would need to be updated to point to the archive \(expensive, transactionally risky\) or deleted \(cascading data loss\). Keeping rows but using standard unique constraints blocks re-use of identifiers \(e.g., freeing up an email address for a new account\). The solution is database-native partial indexing. PostgreSQL's partial indexes are ideal: \`CREATE UNIQUE INDEX idx\_active\_email ON users\(email\) WHERE deleted\_at IS NULL\`. This enforces uniqueness only among active rows, allowing deleted emails to be reclaimed. For MySQL prior to 8.0.13, this was impossible natively \(workarounds: nullable column tricks or triggers\). With 8.0.13\+, functional indexes allow similar patterns. Always ensure queries filter \`WHERE deleted\_at IS NULL\` to utilize these indexes; otherwise, you get table scans on soft-deleted data.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T06:35:30.354699+00:00— report_created — created