Fix for Bug #6123.
authorKevin Grittner <[email protected]>
Tue, 26 Jul 2011 18:01:01 +0000 (13:01 -0500)
committerKevin Grittner <[email protected]>
Tue, 26 Jul 2011 18:01:01 +0000 (13:01 -0500)
This allows a DELETE to complete even if its BEFORE DELETE trigger
updates the row, and throws an error if an UPDATE's BEFORE UPDATE
trigger updates the row.

src/backend/executor/execMain.c
src/backend/executor/nodeModifyTable.c
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index eacd863647d2b09de71670806d8cbe84a93159ce..5462da623e9d530dfb88a1517dd1c26e2c7c03ce 100644 (file)
@@ -1847,8 +1847,16 @@ EvalPlanQualFetch(EState *estate, Relation relation, int lockmode,
            switch (test)
            {
                case HeapTupleSelfUpdated:
-                   /* treat it as deleted; do not process */
                    ReleaseBuffer(buffer);
+                   if (!ItemPointerEquals(&update_ctid, &tuple.t_self))
+                   {
+                       /* it was updated, so look at the updated version */
+                       tuple.t_self = update_ctid;
+                       /* updated row should have xmin matching this xmax */
+                       priorXmax = update_xmax;
+                       continue;
+                   }
+                   /* treat it as deleted; do not process */
                    return NULL;
 
                case HeapTupleMayBeUpdated:
index 070f27c66c96a4a9074e934782fa629571af8289..f06c68e6cc24658af451ed83048f3ef01d6e28d3 100644 (file)
@@ -353,7 +353,30 @@ ldelete:;
                             true /* wait for commit */ );
        switch (result)
        {
+           /*
+            * Don't allow updates to a row during its BEFORE DELETE trigger
+            * to prevent the deletion.  One example of where this might
+            * happen is that the BEFORE DELETE trigger could delete a child
+            * row, and a trigger on that child row might update some count or
+            * status column in the row originally being deleted.
+            */
            case HeapTupleSelfUpdated:
+               if (!ItemPointerEquals(tupleid, &update_ctid))
+               {
+                   HeapTuple   copyTuple;
+
+                   estate->es_output_cid = GetCurrentCommandId(false);
+                   copyTuple = EvalPlanQualFetch(estate,
+                                                 resultRelationDesc,
+                                                 LockTupleExclusive,
+                                                 &update_ctid,
+                                                 update_xmax);
+                   if (copyTuple != NULL)
+                   {
+                       *tupleid = update_ctid = copyTuple->t_self;
+                       goto ldelete;
+                   }
+               }
                /* already deleted by self; nothing to do */
                return NULL;
 
@@ -570,7 +593,25 @@ lreplace:;
        switch (result)
        {
            case HeapTupleSelfUpdated:
-               /* already deleted by self; nothing to do */
+               /*
+                * There is no sensible action to take if the BEFORE UPDATE
+                * trigger for a row issues another UPDATE for the same row,
+                * either directly or by performing DML which fires other
+                * triggers which do the update.  We don't want to discard the
+                * original UPDATE while keeping the triggered actions based
+                * on its update; and it would be no better to allow the
+                * original UPDATE while discarding some of its triggered
+                * actions.
+                */
+               if (!ItemPointerEquals(tupleid, &update_ctid)
+                   && GetCurrentCommandId(false) != estate->es_output_cid)
+               {
+                   ereport(ERROR,
+                           (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
+                            errmsg("cannot update a row from its BEFORE UPDATE trigger"),
+                            errhint("Consider moving updates to the AFTER UPDATE trigger.")));
+               }
+               /* already deleted or updated by self; nothing to do */
                return NULL;
 
            case HeapTupleMayBeUpdated:
index c9e8c1a141b9eccd0f5f83a2ce12c3192efe959b..a70a0e1cf48ad03e54d6cc6cc4931f3e7601aa19 100644 (file)
@@ -1414,3 +1414,98 @@ NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to view city_view
 drop cascades to view european_city_view
 DROP TABLE country_table;
+--
+-- Test updates to rows during firing of BEFORE ROW triggers.
+--
+create table parent (aid int not null primary key,
+                     val1 text,
+                     val2 text,
+                     val3 text,
+                     val4 text,
+                     bcnt int not null default 0);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "parent_pkey" for table "parent"
+create table child (bid int not null primary key,
+                    aid int not null,
+                    val1 text);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "child_pkey" for table "child"
+create function parent_upd_func()
+  returns trigger language plpgsql as
+$$
+begin
+  if old.val1 <> new.val1 then
+    new.val2 = new.val1;
+    delete from child where child.aid = new.aid and child.val1 = new.val1;
+  end if;
+  return new;
+end;
+$$;
+create trigger parent_upd_trig before update On parent
+  for each row execute procedure parent_upd_func();
+create function parent_del_func()
+  returns trigger language plpgsql as
+$$
+begin
+  delete from child where aid = old.aid;
+  return old;
+end;
+$$;
+create trigger parent_del_trig before delete On parent
+  for each row execute procedure parent_del_func();
+create function child_ins_func()
+  returns trigger language plpgsql as
+$$
+begin
+  update parent set bcnt = bcnt + 1 where aid = new.aid;
+  return new;
+end;
+$$;
+create trigger child_ins_trig after insert on child
+  for each row execute procedure child_ins_func();
+create function child_del_func()
+  returns trigger language plpgsql as
+$$
+begin
+  update parent set bcnt = bcnt - 1 where aid = old.aid;
+  return old;
+end;
+$$;
+create trigger child_del_trig after delete on child
+  for each row execute procedure child_del_func();
+insert into parent values (1, 'a', 'a', 'a', 'a', 0);
+insert into child values (10, 1, 'b');
+select * from parent; select * from child;
+ aid | val1 | val2 | val3 | val4 | bcnt 
+-----+------+------+------+------+------
+   1 | a    | a    | a    | a    |    1
+(1 row)
+
+ bid | aid | val1 
+-----+-----+------
+  10 |   1 | b
+(1 row)
+
+update parent set val1 = 'b' where aid = 1;
+ERROR:  cannot update a row from its BEFORE UPDATE trigger
+HINT:  Consider moving updates to the AFTER UPDATE trigger.
+select * from parent; select * from child;
+ aid | val1 | val2 | val3 | val4 | bcnt 
+-----+------+------+------+------+------
+   1 | a    | a    | a    | a    |    1
+(1 row)
+
+ bid | aid | val1 
+-----+-----+------
+  10 |   1 | b
+(1 row)
+
+delete from parent where aid = 1;
+select * from parent; select * from child;
+ aid | val1 | val2 | val3 | val4 | bcnt 
+-----+------+------+------+------+------
+(0 rows)
+
+ bid | aid | val1 
+-----+-----+------
+(0 rows)
+
+drop table parent, child;
index 28928d5a936072bfbbc24b5c60e6d8a707a2ee82..d6a4f2558bcb00b576f8a2facae07e00c9a6b2c5 100644 (file)
@@ -935,3 +935,74 @@ SELECT * FROM city_view;
 
 DROP TABLE city_table CASCADE;
 DROP TABLE country_table;
+
+--
+-- Test updates to rows during firing of BEFORE ROW triggers.
+--
+
+create table parent (aid int not null primary key,
+                     val1 text,
+                     val2 text,
+                     val3 text,
+                     val4 text,
+                     bcnt int not null default 0);
+create table child (bid int not null primary key,
+                    aid int not null,
+                    val1 text);
+
+create function parent_upd_func()
+  returns trigger language plpgsql as
+$$
+begin
+  if old.val1 <> new.val1 then
+    new.val2 = new.val1;
+    delete from child where child.aid = new.aid and child.val1 = new.val1;
+  end if;
+  return new;
+end;
+$$;
+create trigger parent_upd_trig before update On parent
+  for each row execute procedure parent_upd_func();
+
+create function parent_del_func()
+  returns trigger language plpgsql as
+$$
+begin
+  delete from child where aid = old.aid;
+  return old;
+end;
+$$;
+create trigger parent_del_trig before delete On parent
+  for each row execute procedure parent_del_func();
+
+create function child_ins_func()
+  returns trigger language plpgsql as
+$$
+begin
+  update parent set bcnt = bcnt + 1 where aid = new.aid;
+  return new;
+end;
+$$;
+create trigger child_ins_trig after insert on child
+  for each row execute procedure child_ins_func();
+
+create function child_del_func()
+  returns trigger language plpgsql as
+$$
+begin
+  update parent set bcnt = bcnt - 1 where aid = old.aid;
+  return old;
+end;
+$$;
+create trigger child_del_trig after delete on child
+  for each row execute procedure child_del_func();
+
+insert into parent values (1, 'a', 'a', 'a', 'a', 0);
+insert into child values (10, 1, 'b');
+select * from parent; select * from child;
+update parent set val1 = 'b' where aid = 1;
+select * from parent; select * from child;
+delete from parent where aid = 1;
+select * from parent; select * from child;
+
+drop table parent, child;