From ef2a98d8f07afb9d9ccdb11f218072b1c384599b Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
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');
  <waiting ...>
 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

