Repair problems occurring when multiple RI updates have to be done to the same
authorTom Lane <tgl@sss.pgh.pa.us>
Wed, 15 Aug 2007 19:16:12 +0000 (19:16 +0000)
committerTom Lane <tgl@sss.pgh.pa.us>
Wed, 15 Aug 2007 19:16:12 +0000 (19:16 +0000)
row within one query: we were firing check triggers before all the updates
were done, leading to bogus failures.  Fix by making the triggers queued by
an RI update go at the end of the outer query's trigger event list, thereby
effectively making the processing "breadth-first".  This was indeed how it
worked pre-8.0, so the bug does not occur in the 7.x branches.
Per report from Pavel Stehule.

src/backend/commands/trigger.c
src/backend/executor/spi.c
src/backend/utils/adt/ri_triggers.c
src/include/executor/spi.h
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index 232e6a83354455a79a773059c1ee22349692fad8..ac8195b78f619748fc0a04f4731903e8e81f8ce5 100644 (file)
@@ -2340,10 +2340,12 @@ AfterTriggerEndQuery(void)
         * SET CONSTRAINTS ... IMMEDIATE: all events we have decided to defer
         * will be available for it to fire.
         *
+        * We loop in case a trigger queues more events.
+        *
         * If we find no firable events, we don't have to increment firing_counter.
         */
        events = &afterTriggers->query_stack[afterTriggers->query_depth];
-       if (afterTriggerMarkEvents(events, &afterTriggers->events, true))
+       while (afterTriggerMarkEvents(events, &afterTriggers->events, true))
        {
                CommandId               firing_id = afterTriggers->firing_counter++;
 
@@ -2834,7 +2836,7 @@ AfterTriggerSetState(ConstraintsSetStmt *stmt)
        {
                AfterTriggerEventList *events = &afterTriggers->events;
 
-               if (afterTriggerMarkEvents(events, NULL, true))
+               while (afterTriggerMarkEvents(events, NULL, true))
                {
                        CommandId               firing_id = afterTriggers->firing_counter++;
 
index 9b3bad74fcd8b7c30a99e6d35f6e63e249754323..1ad4130eb4713fdec020069288290cd144b5be61 100644 (file)
@@ -39,9 +39,9 @@ static void _SPI_prepare_plan(const char *src, _SPI_plan *plan);
 static int _SPI_execute_plan(_SPI_plan *plan,
                                                         Datum *Values, const char *Nulls,
                                                         Snapshot snapshot, Snapshot crosscheck_snapshot,
-                                                        bool read_only, int tcount);
+                                                        bool read_only, bool fire_triggers, int tcount);
 
-static int _SPI_pquery(QueryDesc *queryDesc, int tcount);
+static int _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, int tcount);
 
 static void _SPI_error_callback(void *arg);
 
@@ -301,7 +301,7 @@ SPI_execute(const char *src, bool read_only, int tcount)
 
        res = _SPI_execute_plan(&plan, NULL, NULL,
                                                        InvalidSnapshot, InvalidSnapshot,
-                                                       read_only, tcount);
+                                                       read_only, true, tcount);
 
        _SPI_end_call(true);
        return res;
@@ -334,7 +334,7 @@ SPI_execute_plan(void *plan, Datum *Values, const char *Nulls,
        res = _SPI_execute_plan((_SPI_plan *) plan,
                                                        Values, Nulls,
                                                        InvalidSnapshot, InvalidSnapshot,
-                                                       read_only, tcount);
+                                                       read_only, true, tcount);
 
        _SPI_end_call(true);
        return res;
@@ -349,9 +349,12 @@ SPI_execp(void *plan, Datum *Values, const char *Nulls, int tcount)
 
 /*
  * SPI_execute_snapshot -- identical to SPI_execute_plan, except that we allow
- * the caller to specify exactly which snapshots to use.  This is currently
- * not documented in spi.sgml because it is only intended for use by RI
- * triggers.
+ * the caller to specify exactly which snapshots to use.  Also, the caller
+ * may specify that AFTER triggers should be queued as part of the outer
+ * query rather than being fired immediately at the end of the command.
+ *
+ * This is currently not documented in spi.sgml because it is only intended
+ * for use by RI triggers.
  *
  * Passing snapshot == InvalidSnapshot will select the normal behavior of
  * fetching a new snapshot for each query.
@@ -360,7 +363,7 @@ int
 SPI_execute_snapshot(void *plan,
                                         Datum *Values, const char *Nulls,
                                         Snapshot snapshot, Snapshot crosscheck_snapshot,
-                                        bool read_only, int tcount)
+                                        bool read_only, bool fire_triggers, int tcount)
 {
        int                     res;
 
@@ -377,7 +380,7 @@ SPI_execute_snapshot(void *plan,
        res = _SPI_execute_plan((_SPI_plan *) plan,
                                                        Values, Nulls,
                                                        snapshot, crosscheck_snapshot,
-                                                       read_only, tcount);
+                                                       read_only, fire_triggers, tcount);
 
        _SPI_end_call(true);
        return res;
@@ -1317,12 +1320,14 @@ _SPI_prepare_plan(const char *src, _SPI_plan *plan)
  *             behavior of taking a new snapshot for each query.
  * crosscheck_snapshot: for RI use, all others pass InvalidSnapshot
  * read_only: TRUE for read-only execution (no CommandCounterIncrement)
+ * fire_triggers: TRUE to fire AFTER triggers at end of query (normal case);
+ *             FALSE means any AFTER triggers are postponed to end of outer query
  * tcount: execution tuple-count limit, or 0 for none
  */
 static int
 _SPI_execute_plan(_SPI_plan *plan, Datum *Values, const char *Nulls,
                                  Snapshot snapshot, Snapshot crosscheck_snapshot,
-                                 bool read_only, int tcount)
+                                 bool read_only, bool fire_triggers, int tcount)
 {
        volatile int res = 0;
        Snapshot        saveActiveSnapshot;
@@ -1473,7 +1478,7 @@ _SPI_execute_plan(_SPI_plan *plan, Datum *Values, const char *Nulls,
                                                                                        crosscheck_snapshot,
                                                                                        dest,
                                                                                        paramLI, false);
-                                       res = _SPI_pquery(qdesc,
+                                       res = _SPI_pquery(qdesc, fire_triggers,
                                                                          queryTree->canSetTag ? tcount : 0);
                                        FreeQueryDesc(qdesc);
                                }
@@ -1506,7 +1511,7 @@ fail:
 }
 
 static int
-_SPI_pquery(QueryDesc *queryDesc, int tcount)
+_SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, int tcount)
 {
        int                     operation = queryDesc->operation;
        int                     res;
@@ -1540,7 +1545,8 @@ _SPI_pquery(QueryDesc *queryDesc, int tcount)
                ResetUsage();
 #endif
 
-       AfterTriggerBeginQuery();
+       if (fire_triggers)
+               AfterTriggerBeginQuery();
 
        ExecutorStart(queryDesc, false);
 
@@ -1558,7 +1564,8 @@ _SPI_pquery(QueryDesc *queryDesc, int tcount)
        ExecutorEnd(queryDesc);
 
        /* Take care of any queued AFTER triggers */
-       AfterTriggerEndQuery();
+       if (fire_triggers)
+               AfterTriggerEndQuery();
 
        if (queryDesc->dest->mydest == SPI)
        {
index 5a7b46bff9613ae5fb9a6f0b877f6883abcf5b70..379ea300b595e7aba332edd222039f8196cc3130 100644 (file)
@@ -2709,7 +2709,7 @@ RI_Initial_Check(FkConstraint *fkconstraint, Relation rel, Relation pkrel)
                                                                          NULL, NULL,
                                                                          CopySnapshot(GetLatestSnapshot()),
                                                                          InvalidSnapshot,
-                                                                         true, 1);
+                                                                         true, false, 1);
 
        /* Check result */
        if (spi_result != SPI_OK_SELECT)
@@ -3139,7 +3139,7 @@ ri_PerformCheck(RI_QueryKey *qkey, void *qplan,
        spi_result = SPI_execute_snapshot(qplan,
                                                                          vals, nulls,
                                                                          test_snapshot, crosscheck_snapshot,
-                                                                         false, limit);
+                                                                         false, false, limit);
 
        /* Restore UID */
        SetUserId(save_uid);
index 6c1d651bc20876884ef334d8efbfc0f4e330ae8b..0965f41e1122fc67e815b50ef86f546904efed9d 100644 (file)
@@ -92,7 +92,7 @@ extern int    SPI_execute_snapshot(void *plan,
                                                                 Datum *Values, const char *Nulls,
                                                                 Snapshot snapshot,
                                                                 Snapshot crosscheck_snapshot,
-                                                                bool read_only, int tcount);
+                                                                bool read_only, bool fire_triggers, int tcount);
 extern void *SPI_prepare(const char *src, int nargs, Oid *argtypes);
 extern void *SPI_saveplan(void *plan);
 extern int     SPI_freeplan(void *plan);
index a479f4932a82ab587c160c94d3afe46189236c48..72e41a489479d56a9c721c1587c76bd4a9d001ba 100644 (file)
@@ -646,7 +646,6 @@ SELECT * from FKTABLE;
 UPDATE PKTABLE set ptest2=5 where ptest2=2;
 ERROR:  insert or update on table "fktable" violates foreign key constraint "constrname3"
 DETAIL:  Key (ftest1,ftest2,ftest3)=(1,-1,3) is not present in table "pktable".
-CONTEXT:  SQL statement "UPDATE ONLY "public"."fktable" SET "ftest2" = DEFAULT WHERE "ftest1" = $1 AND "ftest2" = $2 AND "ftest3" = $3"
 -- Try to update something that will set default
 UPDATE PKTABLE set ptest1=0, ptest2=5, ptest3=10 where ptest2=2;
 UPDATE PKTABLE set ptest2=10 where ptest2=4;
@@ -1207,3 +1206,71 @@ ROLLBACK TO savept1;
 COMMIT;
 ERROR:  insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
 DETAIL:  Key (fk)=(20) is not present in table "pktable".
+-- test order of firing of FK triggers when several RI-induced changes need to
+-- be made to the same row.  This was broken by subtransaction-related
+-- changes in 8.0.
+CREATE TEMP TABLE users (
+  id INT PRIMARY KEY,
+  name VARCHAR NOT NULL
+);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "users_pkey" for table "users"
+INSERT INTO users VALUES (1, 'Jozko');
+INSERT INTO users VALUES (2, 'Ferko');
+INSERT INTO users VALUES (3, 'Samko');
+CREATE TEMP TABLE tasks (
+  id INT PRIMARY KEY,
+  owner INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL,
+  worker INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL,
+  checked_by INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL
+);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "tasks_pkey" for table "tasks"
+INSERT INTO tasks VALUES (1,1,NULL,NULL);
+INSERT INTO tasks VALUES (2,2,2,NULL);
+INSERT INTO tasks VALUES (3,3,3,3);
+SELECT * FROM tasks;
+ id | owner | worker | checked_by 
+----+-------+--------+------------
+  1 |     1 |        |           
+  2 |     2 |      2 |           
+  3 |     3 |      3 |          3
+(3 rows)
+
+UPDATE users SET id = 4 WHERE id = 3;
+SELECT * FROM tasks;
+ id | owner | worker | checked_by 
+----+-------+--------+------------
+  1 |     1 |        |           
+  2 |     2 |      2 |           
+  3 |     4 |      4 |          4
+(3 rows)
+
+DELETE FROM users WHERE id = 4;
+SELECT * FROM tasks;
+ id | owner | worker | checked_by 
+----+-------+--------+------------
+  1 |     1 |        |           
+  2 |     2 |      2 |           
+  3 |       |        |           
+(3 rows)
+
+-- could fail with only 2 changes to make, if row was already updated
+BEGIN;
+UPDATE tasks set id=id WHERE id=2;
+SELECT * FROM tasks;
+ id | owner | worker | checked_by 
+----+-------+--------+------------
+  1 |     1 |        |           
+  3 |       |        |           
+  2 |     2 |      2 |           
+(3 rows)
+
+DELETE FROM users WHERE id = 2;
+SELECT * FROM tasks;
+ id | owner | worker | checked_by 
+----+-------+--------+------------
+  1 |     1 |        |           
+  3 |       |        |           
+  2 |       |        |           
+(3 rows)
+
+COMMIT;
index dee046a1952a845c4e4e4e99689040d8a8cf6431..562796ecceb7bbd4e5d3392824d9ecfd38388b2b 100644 (file)
@@ -855,3 +855,45 @@ ROLLBACK TO savept1;
 
 -- should catch error from initial INSERT
 COMMIT;
+
+-- test order of firing of FK triggers when several RI-induced changes need to
+-- be made to the same row.  This was broken by subtransaction-related
+-- changes in 8.0.
+
+CREATE TEMP TABLE users (
+  id INT PRIMARY KEY,
+  name VARCHAR NOT NULL
+);
+
+INSERT INTO users VALUES (1, 'Jozko');
+INSERT INTO users VALUES (2, 'Ferko');
+INSERT INTO users VALUES (3, 'Samko');
+
+CREATE TEMP TABLE tasks (
+  id INT PRIMARY KEY,
+  owner INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL,
+  worker INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL,
+  checked_by INT REFERENCES users ON UPDATE CASCADE ON DELETE SET NULL
+);
+
+INSERT INTO tasks VALUES (1,1,NULL,NULL);
+INSERT INTO tasks VALUES (2,2,2,NULL);
+INSERT INTO tasks VALUES (3,3,3,3);
+
+SELECT * FROM tasks;
+
+UPDATE users SET id = 4 WHERE id = 3;
+
+SELECT * FROM tasks;
+
+DELETE FROM users WHERE id = 4;
+
+SELECT * FROM tasks;
+
+-- could fail with only 2 changes to make, if row was already updated
+BEGIN;
+UPDATE tasks set id=id WHERE id=2;
+SELECT * FROM tasks;
+DELETE FROM users WHERE id = 2;
+SELECT * FROM tasks;
+COMMIT;