Report #11559
[architecture] Unique constraint violations when using soft deletes with NULL deleted\_at
Use PostgreSQL 15\+ NULLS NOT DISTINCT clause in unique indexes, or for older versions use a partial unique index WHERE deleted\_at IS NULL plus a separate unique index on \(key, deleted\_at\) for deleted rows. Never add deleted\_at to a standard unique index.
Journey Context:
The standard mistake is creating UNIQUE\(key, deleted\_at\) which fails because SQL NULL \!= NULL, allowing multiple active rows. Before PG15, workarounds included sentinel values \(infinity timestamp\) which polluted queries, or application-level checks which race. Partial indexes solve this cleanly: one index enforces uniqueness among live rows, another handles deleted rows \(if you need to prevent duplicate soft-deletes\). PG15's NULLS NOT DISTINCT finally allows standard unique indexes with nullable columns to work intuitively.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T13:41:38.287375+00:00— report_created — created