From: Kevin Grittner Date: Tue, 26 Jul 2011 18:01:01 +0000 (-0500) Subject: Fix for Bug #6123. X-Git-Url: http://git.postgresql.org/gitweb/static/gitweb.js?a=commitdiff_plain;h=dbddc5dececab6960f6af3c082ca81255aaadf76;p=users%2Fkgrittn%2Fpostgres.git Fix for Bug #6123. 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. --- diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index eacd863647..5462da623e 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -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: diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 070f27c66c..f06c68e6cc 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -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: diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index c9e8c1a141..a70a0e1cf4 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -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; diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 28928d5a93..d6a4f2558b 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -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;