From ef2a98d8f07afb9d9ccdb11f218072b1c384599b Mon Sep 17 00:00:00 2001 From: Yugo Nagata Date: Tue, 1 Jul 2025 18:36:01 +0900 Subject: [PATCH v9 4/4] Improve error reporting for unique key violations in system catalogs Previously, when a unique constraint violation occurred in a system catalog, typically due to a concurrent session creating an object with the same key, a low-level error like the following was raised by nbtree code: ERROR: duplicate key value violates unique constraint ... However, this message is not very user-friendly, as users are not directly inserting rows into the system catalogs. This commit improves the error reporting by generating a more descriptive and user-facing error message in such cases, making it easier to understand the cause of the failure and its likely relation to concurrent DDL activity. --- contrib/test_decoding/expected/replorigin.out | 5 ++-- src/backend/catalog/indexing.c | 24 +++++++++++++++++++ src/backend/executor/execIndexing.c | 19 +++++++++++++++ src/include/executor/executor.h | 5 ++++ .../expected/syscache-update-pruned.out | 2 +- 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/contrib/test_decoding/expected/replorigin.out b/contrib/test_decoding/expected/replorigin.out index c85e1a01b23..9c30e9fb76d 100644 --- a/contrib/test_decoding/expected/replorigin.out +++ b/contrib/test_decoding/expected/replorigin.out @@ -39,8 +39,9 @@ SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); -- ensure duplicate creations fail SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); -ERROR: duplicate key value violates unique constraint "pg_replication_origin_roname_index" -DETAIL: Key (roname)=(regress_test_decoding: regression_slot) already exists. +ERROR: could not create object because a conflicting object already exists +DETAIL: Key (roname)=(regress_test_decoding: regression_slot) conflicts with existing entry in unique index pg_replication_origin_roname_index. +HINT: Another session might have created an object with the same key concurrently. --ensure deletions work (once) SELECT pg_replication_origin_create('regress_test_decoding: temp'); pg_replication_origin_create diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c index 25c4b6bdc87..5d91eeae16f 100644 --- a/src/backend/catalog/indexing.c +++ b/src/backend/catalog/indexing.c @@ -164,6 +164,30 @@ CatalogIndexInsert(CatalogIndexState indstate, HeapTuple heapTuple, values, isnull); + /* Check if a concurrent command inserted an entry with the same key */ + if (index->rd_index->indisunique && IsCatalogRelation(heapRelation)) + { + bool satisfied; + EState *estate = CreateExecutorState(); + + BuildSpeculativeIndexInfo(index, indexInfo); + satisfied = check_unique_constraint(heapRelation, + index, indexInfo, + &(heapTuple->t_self), values, isnull, + estate); + + if (!satisfied) + { + char *key_desc = BuildIndexValueDescription(index, values, isnull); + ereport(ERROR, + (errcode(ERRCODE_UNIQUE_VIOLATION), + errmsg("could not create object because a conflicting object already exists"), + errdetail("Key %s conflicts with existing entry in unique index %s.", + key_desc, RelationGetRelationName(index)), + errhint("Another session might have created an object with the same key concurrently."))); + } + } + /* * The index AM does the rest. */ diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c index ca33a854278..f87ab861b44 100644 --- a/src/backend/executor/execIndexing.c +++ b/src/backend/executor/execIndexing.c @@ -965,6 +965,25 @@ check_exclusion_constraint(Relation heap, Relation index, CEOUC_WAIT, false, NULL); } +/* + * Check for violation of a unique constraint + * + * This is a dumbed down version of check_exclusion_or_unique_constraint + * for external callers. They don't need all the special modes. + */ +bool +check_unique_constraint(Relation heap, Relation index, + IndexInfo *indexInfo, + ItemPointer tupleid, + const Datum *values, const bool *isnull, + EState *estate) +{ + return check_exclusion_or_unique_constraint(heap, index, indexInfo, tupleid, + values, isnull, + estate, false, + CEOUC_WAIT, true, NULL); +} + /* * Check existing tuple's index values to see if it really matches the * exclusion condition against the new_values. Returns true if conflict. diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 10dcea037c3..3190548c385 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -751,6 +751,11 @@ extern void check_exclusion_constraint(Relation heap, Relation index, ItemPointer tupleid, const Datum *values, const bool *isnull, EState *estate, bool newIndex); +extern bool check_unique_constraint(Relation heap, Relation index, + IndexInfo *indexInfo, + ItemPointer tupleid, + const Datum *values, const bool *isnull, + EState *estate); /* * prototypes from functions in execReplication.c diff --git a/src/test/modules/injection_points/expected/syscache-update-pruned.out b/src/test/modules/injection_points/expected/syscache-update-pruned.out index 231545a6cbb..79fd4bff43e 100644 --- a/src/test/modules/injection_points/expected/syscache-update-pruned.out +++ b/src/test/modules/injection_points/expected/syscache-update-pruned.out @@ -46,7 +46,7 @@ step wakegrant4: SELECT FROM injection_points_wakeup('heap_update-before-pin'); step grant1: <... completed> -ERROR: duplicate key value violates unique constraint "pg_class_oid_index" +ERROR: could not create object because a conflicting object already exists step wakegrant4: <... completed> starting permutation: snap3 cachefill1 at2 mkrels4 r3 waitprunable4 vac4 grant1 wakeinval4 at4 wakegrant4 inspect4 -- 2.43.0