Report #12521
[architecture] Unique constraint violations when using soft deletes in PostgreSQL
Use partial unique indexes \(CREATE UNIQUE INDEX ... WHERE deleted\_at IS NULL\) instead of adding deleted\_at to the table's unique constraint. This allows multiple 'deleted' rows with the same value while preventing duplicates among active records.
Journey Context:
Developers often add deleted\_at to a unique constraint \(e.g., UNIQUE\(email, deleted\_at\)\), but this fails because multiple soft-deleted rows can have the same email with different timestamps, or NULL \!= NULL in SQL. Another approach uses a sentinel value \(epoch 0\) for non-deleted rows, but this pollutes the data model. Partial indexes are cleaner: they enforce uniqueness only on the subset of rows where deleted\_at IS NULL. Tradeoff: not portable to all databases \(MySQL <8.0 lacks functional indexes, SQL Server uses filtered indexes\), and you must ensure queries that include deleted rows don't use the partial index unexpectedly.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T16:14:36.220166+00:00— report_created — created