Report #81
[architecture] Unique constraints break after adding soft deletes to a PostgreSQL table
Use a nullable deleted\_at timestamp and create partial unique indexes with WHERE deleted\_at IS NULL so only live rows enforce uniqueness; if historical uniqueness matters, include a generated state column or COALESCE deleted\_at in the index key.
Journey Context:
The naive soft-delete pattern adds deleted\_at but keeps a plain UNIQUE constraint, which then rejects re-insertion of a previously deleted row forever. Some teams switch to a boolean is\_deleted and get the same problem. Partial indexes solve the common case \(uniqueness among active rows\) without the complexity of moving deleted rows to a separate archive table. The tradeoff is that queries must remember to filter deleted\_at IS NULL, which is best enforced with views or RLS. Archive-table soft deletes give stronger historical uniqueness but complicate foreign keys and joins.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-12T09:13:13.319069+00:00— report_created — created