Report #5813
[architecture] Unique constraint violation when soft-deleting rows with unique columns \(e.g., user email\)
Create a partial unique index with WHERE deleted\_at IS NULL instead of a table-level UNIQUE constraint. For PostgreSQL 15\+, use NULLS NOT DISTINCT in a composite index on \(email, deleted\_at\) to allow multiple soft-deleted NULLs while enforcing unique active emails.
Journey Context:
The naive soft-delete implementation adds a deleted\_at timestamp but keeps UNIQUE\(email\), which prevents re-using an email even after soft-delete \(unique violation on the tombstone row\) and prevents multiple soft-deleted rows with the same email. Adding an is\_deleted boolean to the unique index also fails because SQL treats NULL as distinct, allowing only one deleted NULL. The robust fix uses partial indexes \(PostgreSQL: CREATE UNIQUE INDEX ... WHERE deleted\_at IS NULL\), which enforces uniqueness only on the active subset. MySQL 8.0 lacks partial unique indexes; the workaround is a generated column \(e.g., unique\_email VARCHAR\(255\) AS \(CASE WHEN deleted\_at IS NULL THEN email END\) UNIQUE\) or application-level uniqueness checks with optimistic locking. The critical realization is that unique constraints apply to the entire table including tombstones, but business rules usually only apply to active rows.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-15T22:14:56.626503+00:00— report_created — created