]> granicus.if.org Git - postgresql/commitdiff
Fix incorrect checking of deferred exclusion constraint after a HOT update.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 11 May 2015 16:25:28 +0000 (12:25 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 11 May 2015 16:25:28 +0000 (12:25 -0400)
If a row that potentially violates a deferred exclusion constraint is
HOT-updated later in the same transaction, the exclusion constraint would
be reported as violated when the check finally occurs, even if the row(s)
the new row originally conflicted with have since been removed.  This
happened because the wrong TID was passed to check_exclusion_constraint(),
causing the live HOT-updated row to be seen as a conflicting row rather
than recognized as the row-under-test.

Per bug #13148 from Evan Martin.  It's been broken since exclusion
constraints were invented, so back-patch to all supported branches.

src/backend/commands/constraint.c
src/test/regress/input/constraints.source
src/test/regress/output/constraints.source

index 2d376089ff733cdb0be1d73de2cde9405d16b591..3b0b88bc25909c7286c9c97f0b6343a45d954303 100644 (file)
@@ -88,9 +88,10 @@ unique_key_recheck(PG_FUNCTION_ARGS)
         * because this trigger gets queued only in response to index insertions;
         * which means it does not get queued for HOT updates.  The row we are
         * called for might now be dead, but have a live HOT child, in which case
-        * we still need to make the check.  Therefore we have to use
-        * heap_hot_search, not just HeapTupleSatisfiesVisibility as is done in
-        * the comparable test in RI_FKey_check.
+        * we still need to make the check --- effectively, we're applying the
+        * check against the live child row, although we can use the values from
+        * this row since by definition all columns of interest to us are the
+        * same.
         *
         * This might look like just an optimization, because the index AM will
         * make this identical test before throwing an error.  But it's actually
@@ -158,7 +159,9 @@ unique_key_recheck(PG_FUNCTION_ARGS)
        {
                /*
                 * Note: this is not a real insert; it is a check that the index entry
-                * that has already been inserted is unique.
+                * that has already been inserted is unique.  Passing t_self is
+                * correct even if t_self is now dead, because that is the TID the
+                * index will know about.
                 */
                index_insert(indexRel, values, isnull, &(new_row->t_self),
                                         trigdata->tg_relation, UNIQUE_CHECK_EXISTING);
@@ -167,10 +170,12 @@ unique_key_recheck(PG_FUNCTION_ARGS)
        {
                /*
                 * For exclusion constraints we just do the normal check, but now it's
-                * okay to throw error.
+                * okay to throw error.  In the HOT-update case, we must use the live
+                * HOT child's TID here, else check_exclusion_constraint will think
+                * the child is a conflict.
                 */
                check_exclusion_constraint(trigdata->tg_relation, indexRel, indexInfo,
-                                                                  &(new_row->t_self), values, isnull,
+                                                                  &tmptid, values, isnull,
                                                                   estate, false, false);
        }
 
index 0d278212c02a4801cd0919b4c2a134b448a2ac42..52e7aef59c22bea5331f2712aac67e484ea37622 100644 (file)
@@ -403,6 +403,7 @@ DROP TABLE circles;
 
 CREATE TABLE deferred_excl (
   f1 int,
+  f2 int,
   CONSTRAINT deferred_excl_con EXCLUDE (f1 WITH =) INITIALLY DEFERRED
 );
 
@@ -417,6 +418,15 @@ INSERT INTO deferred_excl VALUES(3);
 INSERT INTO deferred_excl VALUES(3); -- no fail here
 COMMIT; -- should fail here
 
+-- bug #13148: deferred constraint versus HOT update
+BEGIN;
+INSERT INTO deferred_excl VALUES(2, 1); -- no fail here
+DELETE FROM deferred_excl WHERE f1 = 2 AND f2 IS NULL; -- remove old row
+UPDATE deferred_excl SET f2 = 2 WHERE f1 = 2;
+COMMIT; -- should not fail
+
+SELECT * FROM deferred_excl;
+
 ALTER TABLE deferred_excl DROP CONSTRAINT deferred_excl_con;
 
 -- This should fail, but worth testing because of HOT updates
index d164b90af78e5cb818803e1f1bbadb90fe87c05a..9a022a5a5a23f7912cdae1d8078ebbbd6ce60e61 100644 (file)
@@ -547,6 +547,7 @@ DROP TABLE circles;
 -- Check deferred exclusion constraint
 CREATE TABLE deferred_excl (
   f1 int,
+  f2 int,
   CONSTRAINT deferred_excl_con EXCLUDE (f1 WITH =) INITIALLY DEFERRED
 );
 NOTICE:  CREATE TABLE / EXCLUDE will create implicit index "deferred_excl_con" for table "deferred_excl"
@@ -566,6 +567,19 @@ INSERT INTO deferred_excl VALUES(3); -- no fail here
 COMMIT; -- should fail here
 ERROR:  conflicting key value violates exclusion constraint "deferred_excl_con"
 DETAIL:  Key (f1)=(3) conflicts with existing key (f1)=(3).
+-- bug #13148: deferred constraint versus HOT update
+BEGIN;
+INSERT INTO deferred_excl VALUES(2, 1); -- no fail here
+DELETE FROM deferred_excl WHERE f1 = 2 AND f2 IS NULL; -- remove old row
+UPDATE deferred_excl SET f2 = 2 WHERE f1 = 2;
+COMMIT; -- should not fail
+SELECT * FROM deferred_excl;
+ f1 | f2 
+----+----
+  1 |   
+  2 |  2
+(2 rows)
+
 ALTER TABLE deferred_excl DROP CONSTRAINT deferred_excl_con;
 -- This should fail, but worth testing because of HOT updates
 UPDATE deferred_excl SET f1 = 3;