Erreur PostgreSQL : no unique or exclusion constraint matching the ON CONFLICT specification
ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
INSERT ... ON CONFLICT a besoin d'un arbitre : une contrainte d'unicité (ou d'exclusion) qui définit ce que « conflit » signifie. La cible du conflit que vous avez écrite doit en correspondre une exactement — même ensemble de colonnes, même expression, même prédicat partiel. Aucune correspondance, aucun upsert.
Ce que signifie cette erreur
PostgreSQL ne devine pas quelle contrainte vous voulez dire. ON CONFLICT (email) exige un index unique sur exactement (email) :
- Un index non unique sur email ne compte pas.
- Un index unique sur (tenant_id, email) ne correspond pas à une cible de
(email)— ni l'inverse. - Un index sur expression sur
lower(email)ne correspond qu'à la cible(lower(email)). - Un index unique partiel (
WHERE deleted_at IS NULL) ne correspond qu'à une cible avec la même clause WHERE.
Causes fréquentes
- La contrainte d'unicité n'existe tout simplement pas — l'unicité a été supposée, jamais déclarée.
- L'ensemble de colonnes cible diffère de l'index (oublier la colonne tenant est le classique dans les schémas multi-tenant).
- L'index est sur une expression ou partiel, et la cible du conflit ne le répète pas.
Comment la diagnostiquer
\d users -- liste les index et contraintes avec leur définition exacte
Comparez la cible du conflit dans l'INSERT en échec aux définitions des index, en vérifiant colonnes, expressions et clauses WHERE littéralement.
Comment la corriger
-- La contrainte manque → créez-la (en production : CONCURRENTLY)
CREATE UNIQUE INDEX CONCURRENTLY users_email_key ON users (email);
-- Index sur expression → la cible doit la répéter :
INSERT INTO users (email) VALUES ('[email protected]')
ON CONFLICT (lower(email)) DO NOTHING;
-- Index partiel → la cible doit répéter le prédicat :
INSERT INTO users (email) VALUES ('[email protected]')
ON CONFLICT (email) WHERE deleted_at IS NULL DO NOTHING;
Alternativement, nommez directement la contrainte — ON CONFLICT ON CONSTRAINT users_email_key — bien que la forme par colonnes survive mieux aux renommages de contraintes.
