Report #55474
[architecture] How to enforce unique constraints \(e.g., email\) with soft-delete without allowing duplicate emails across active and deleted rows
Use a partial unique index that excludes soft-deleted rows: \`CREATE UNIQUE INDEX idx\_users\_email ON users\(email\) WHERE deleted\_at IS NULL;\`. Never include \`deleted\_at\` in a composite unique key, as SQL NULL \!= NULL semantics would allow duplicate active records.
Journey Context:
The naive approach—adding \`deleted\_at\` to a composite unique index \`\(email, deleted\_at\)\`—fails because SQL treats NULL \!= NULL, allowing multiple rows with the same email and NULL deleted\_at. Another anti-pattern is using a \`deleted\` boolean with a standard unique index, which still permits one active and one 'deleted' record with the same email. The partial index approach is the only solution that maintains true uniqueness among active records while ignoring soft-deleted history. Tradeoff: requires database support for partial indexes \(PostgreSQL, SQL Server, SQLite; MySQL 8.0.13\+ supports functional indexes but partial index emulation is complex\).
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T23:36:26.024610+00:00— report_created — created