From 9c810a2edccaffe0ff48b50d31c47a155e4f9815 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Mon, 4 Jul 2016 16:09:11 -0400
Subject: [PATCH] Fix failure to handle conflicts in non-arbiter exclusion
 constraints.

ExecInsertIndexTuples treated an exclusion constraint as subject to
noDupErr processing even when it was not listed in arbiterIndexes, and
would therefore not error out for a conflict in such a constraint, instead
returning it as an arbiter-index failure.  That led to an infinite loop in
ExecInsert, since ExecCheckIndexConstraints ignored the index as-intended
and therefore didn't throw the expected error.  To fix, make the exclusion
constraint code path use the same condition as the index_insert call does
to decide whether no-error-for-duplicates behavior is appropriate.  While
at it, refactor a little bit to avoid unnecessary list_member_oid calls.
(That surely wouldn't save anything worth noticing, but I find the code
a bit clearer this way.)

Per bug report from Heikki Rauhala.  Back-patch to 9.5 where ON CONFLICT
was introduced.

Report: <4C976D6B-76B4-434C-8052-D009F7B7AEDA@reaktor.fi>
---
 src/backend/executor/execIndexing.c           | 19 +++++++++------
 src/test/regress/expected/insert_conflict.out | 23 +++++++++++++++++++
 src/test/regress/sql/insert_conflict.sql      | 14 +++++++++++
 3 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index c819d19db42..0e2d834ed1d 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -259,6 +259,9 @@ ExecCloseIndices(ResultRelInfo *resultRelInfo)
  *		the same is done for non-deferred constraints, but report
  *		if conflict was speculative or deferred conflict to caller)
  *
+ *		If 'arbiterIndexes' is nonempty, noDupErr applies only to
+ *		those indexes.  NIL means noDupErr applies to all indexes.
+ *
  *		CAUTION: this must not be called for a HOT update.
  *		We can't defend against that here for lack of info.
  *		Should we change the API to make it safer?
@@ -308,19 +311,15 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 	{
 		Relation	indexRelation = relationDescs[i];
 		IndexInfo  *indexInfo;
+		bool		applyNoDupErr;
 		IndexUniqueCheck checkUnique;
 		bool		satisfiesConstraint;
-		bool		arbiter;
 
 		if (indexRelation == NULL)
 			continue;
 
 		indexInfo = indexInfoArray[i];
 
-		/* Record if speculative insertion arbiter */
-		arbiter = list_member_oid(arbiterIndexes,
-								  indexRelation->rd_index->indexrelid);
-
 		/* If the index is marked as read-only, ignore it */
 		if (!indexInfo->ii_ReadyForInserts)
 			continue;
@@ -358,6 +357,12 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 					   values,
 					   isnull);
 
+		/* Check whether to apply noDupErr to this index */
+		applyNoDupErr = noDupErr &&
+			(arbiterIndexes == NIL ||
+			 list_member_oid(arbiterIndexes,
+							 indexRelation->rd_index->indexrelid));
+
 		/*
 		 * The index AM does the actual insertion, plus uniqueness checking.
 		 *
@@ -373,7 +378,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 		 */
 		if (!indexRelation->rd_index->indisunique)
 			checkUnique = UNIQUE_CHECK_NO;
-		else if (noDupErr && (arbiterIndexes == NIL || arbiter))
+		else if (applyNoDupErr)
 			checkUnique = UNIQUE_CHECK_PARTIAL;
 		else if (indexRelation->rd_index->indimmediate)
 			checkUnique = UNIQUE_CHECK_YES;
@@ -407,7 +412,7 @@ ExecInsertIndexTuples(TupleTableSlot *slot,
 			bool		violationOK;
 			CEOUC_WAIT_MODE waitMode;
 
-			if (noDupErr)
+			if (applyNoDupErr)
 			{
 				violationOK = true;
 				waitMode = CEOUC_LIVELOCK_PREVENTING_WAIT;
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 6f4ec867d9f..155774ecfab 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -704,3 +704,26 @@ insert into dropcol(key, keep1, keep2) values(1, '5', 5) on conflict(key)
 
 ;
 DROP TABLE dropcol;
+-- check handling of regular btree constraint along with gist constraint
+create table twoconstraints (f1 int unique, f2 box,
+                             exclude using gist(f2 with &&));
+insert into twoconstraints values(1, '((0,0),(1,1))');
+insert into twoconstraints values(1, '((2,2),(3,3))');  -- fail on f1
+ERROR:  duplicate key value violates unique constraint "twoconstraints_f1_key"
+DETAIL:  Key (f1)=(1) already exists.
+insert into twoconstraints values(2, '((0,0),(1,2))');  -- fail on f2
+ERROR:  conflicting key value violates exclusion constraint "twoconstraints_f2_excl"
+DETAIL:  Key (f2)=((1,2),(0,0)) conflicts with existing key (f2)=((1,1),(0,0)).
+insert into twoconstraints values(2, '((0,0),(1,2))')
+  on conflict on constraint twoconstraints_f1_key do nothing;  -- fail on f2
+ERROR:  conflicting key value violates exclusion constraint "twoconstraints_f2_excl"
+DETAIL:  Key (f2)=((1,2),(0,0)) conflicts with existing key (f2)=((1,1),(0,0)).
+insert into twoconstraints values(2, '((0,0),(1,2))')
+  on conflict on constraint twoconstraints_f2_excl do nothing;  -- do nothing
+select * from twoconstraints;
+ f1 |     f2      
+----+-------------
+  1 | (1,1),(0,0)
+(1 row)
+
+drop table twoconstraints;
diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql
index 94b1b0aadee..190c6055623 100644
--- a/src/test/regress/sql/insert_conflict.sql
+++ b/src/test/regress/sql/insert_conflict.sql
@@ -407,3 +407,17 @@ insert into dropcol(key, keep1, keep2) values(1, '5', 5) on conflict(key)
 ;
 
 DROP TABLE dropcol;
+
+-- check handling of regular btree constraint along with gist constraint
+
+create table twoconstraints (f1 int unique, f2 box,
+                             exclude using gist(f2 with &&));
+insert into twoconstraints values(1, '((0,0),(1,1))');
+insert into twoconstraints values(1, '((2,2),(3,3))');  -- fail on f1
+insert into twoconstraints values(2, '((0,0),(1,2))');  -- fail on f2
+insert into twoconstraints values(2, '((0,0),(1,2))')
+  on conflict on constraint twoconstraints_f1_key do nothing;  -- fail on f2
+insert into twoconstraints values(2, '((0,0),(1,2))')
+  on conflict on constraint twoconstraints_f2_excl do nothing;  -- do nothing
+select * from twoconstraints;
+drop table twoconstraints;
-- 
GitLab