From 6460709341919bc50e5d701a13be3dafd71d2a0c Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Fri, 28 Aug 2026 13:07:34 +0000
Subject: [PATCH v1] Don't use deferrable indexes as additional ON CONFLICT
 arbiters

Commit 90eae926abbb made tuple routing match unparented partition
indexes against the mapped arbiters, so that indexes created by a
concurrent REINDEX are used as arbiters too.  The compatibility check
did not compare enforcement timing, so a partition-local deferrable
unique constraint on the arbiter columns was also chosen, and the
executor then rejected it with "ON CONFLICT does not support deferrable
unique constraints/exclusion constraints as arbiters".  Whether a
given insert failed depended on the data and on index OID order, since
the pre-check only reaches the deferrable index when no earlier arbiter
reports a conflict.

To fix, require indimmediate to match as well.  Indexes differing in
enforcement timing cannot be REINDEX CONCURRENTLY twins, so the case
the matching was added for is unaffected.
---
 src/backend/executor/execPartition.c          |  9 +++++++++
 src/test/regress/expected/insert_conflict.out | 20 +++++++++++++++++++
 src/test/regress/sql/insert_conflict.sql      | 14 +++++++++++++
 3 files changed, 43 insertions(+)

diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 0e938cfea1f..c9630a3ad87 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -512,6 +512,15 @@ IsIndexCompatibleAsArbiter(Relation arbiterIndexRelation,
 	if (arbiterIndexInfo->ii_Unique != indexInfo->ii_Unique)
 		return false;
 
+	/*
+	 * Constraint enforcement timing must match, or we could end up choosing
+	 * a deferrable index as arbiter, which ExecCheckIndexConstraints
+	 * rejects.
+	 */
+	if (arbiterIndexRelation->rd_index->indimmediate !=
+		indexRelation->rd_index->indimmediate)
+		return false;
+
 	/* No support currently for comparing exclusion indexes. */
 	if (arbiterIndexInfo->ii_ExclusionOps != NULL ||
 		indexInfo->ii_ExclusionOps != NULL)
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 34e2e7ee355..9d226042a69 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -1063,6 +1063,26 @@ insert into parted_conflict values (40, 'forty');
 insert into parted_conflict_1 values (40, 'cuarenta')
   on conflict (a) do update set b = excluded.b;
 ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+drop table parted_conflict;
+-- a partition-local deferrable unique constraint on the arbiter columns
+-- must not be picked up as an additional arbiter for routed inserts
+create table parted_conflict (a int, b text, primary key (a)) partition by range (a);
+create table parted_conflict_1 partition of parted_conflict for values from (0) to (1000);
+alter table parted_conflict_1 add constraint parted_conflict_1_a_def unique (a) deferrable;
+insert into parted_conflict values (40, 'forty');
+insert into parted_conflict values (40, 'cuarenta')
+  on conflict (a) do update set b = excluded.b;
+insert into parted_conflict values (41, 'forty-one')
+  on conflict (a) do update set b = excluded.b;
+insert into parted_conflict values (42, 'forty-two') on conflict (a) do nothing;
+select * from parted_conflict order by a;
+ a  |     b     
+----+-----------
+ 40 | cuarenta
+ 41 | forty-one
+ 42 | forty-two
+(3 rows)
+
 drop table parted_conflict;
 -- test whole-row Vars in ON CONFLICT expressions
 create table parted_conflict (a int, b text, c int) partition by range (a);
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index a5a84d1d4b8..76d794fa3e4 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -619,6 +619,20 @@ insert into parted_conflict_1 values (40, 'cuarenta')
   on conflict (a) do update set b = excluded.b;
 drop table parted_conflict;
 
+-- a partition-local deferrable unique constraint on the arbiter columns
+-- must not be picked up as an additional arbiter for routed inserts
+create table parted_conflict (a int, b text, primary key (a)) partition by range (a);
+create table parted_conflict_1 partition of parted_conflict for values from (0) to (1000);
+alter table parted_conflict_1 add constraint parted_conflict_1_a_def unique (a) deferrable;
+insert into parted_conflict values (40, 'forty');
+insert into parted_conflict values (40, 'cuarenta')
+  on conflict (a) do update set b = excluded.b;
+insert into parted_conflict values (41, 'forty-one')
+  on conflict (a) do update set b = excluded.b;
+insert into parted_conflict values (42, 'forty-two') on conflict (a) do nothing;
+select * from parted_conflict order by a;
+drop table parted_conflict;
+
 -- test whole-row Vars in ON CONFLICT expressions
 create table parted_conflict (a int, b text, c int) partition by range (a);
 create table parted_conflict_1 (drp text, c int, a int, b text);
-- 
2.55.0

