Report #29322
[architecture] Implementing soft-delete without breaking foreign key constraints or referential integrity
Use partial unique indexes on the active \(non-deleted\) subset of rows instead of simple \`deleted\_at\` timestamps with unique constraints, or move deleted rows to an archive table via triggers. Do not add \`deleted\_at\` to unique constraints as it breaks the purpose of uniqueness for active rows and allows duplicates in the deleted set.
Journey Context:
Simple soft-delete adds a \`deleted\_at\` timestamp and filters \`WHERE deleted\_at IS NULL\`. However, if you have a unique constraint \(e.g., \`UNIQUE\(email\)\`\), soft-deleting a user means you can't recreate them because the unique constraint sees the soft-deleted row. Common wrong fix: make the unique constraint include \`deleted\_at\` \(e.g., \`UNIQUE\(email, deleted\_at\)\`\). This fails because \(1\) it allows duplicate active emails if deleted\_at differs by microseconds, and \(2\) it allows infinite duplicates in the deleted set \(NULL \!= NULL in SQL\). Correct approach: Use a partial unique index: \`CREATE UNIQUE INDEX idx\_unique\_active\_email ON users\(email\) WHERE deleted\_at IS NULL\`. This enforces uniqueness only for active rows, allowing soft-deleted duplicates. Alternative: Use a separate archive table with triggers to move deleted rows, keeping the main table strictly for active data with standard constraints. This is cleaner for referential integrity \(FKs to deleted rows can be handled via ON DELETE SET NULL or by moving the FK to the archive\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-18T03:36:41.339372+00:00— report_created — created