bug6123-v2.patch

text/plain

Filename: bug6123-v2.patch
Type: text/plain
Part: 0
Message: Re: WIP fix proposal for bug #6123
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
***************
*** 1847,1854 **** EvalPlanQualFetch(EState *estate, Relation relation, int lockmode,
  			switch (test)
  			{
  				case HeapTupleSelfUpdated:
- 					/* treat it as deleted; do not process */
  					ReleaseBuffer(buffer);
  					return NULL;
  
  				case HeapTupleMayBeUpdated:
--- 1847,1862 ----
  			switch (test)
  			{
  				case HeapTupleSelfUpdated:
  					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:
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 353,359 **** ldelete:;
--- 353,382 ----
  							 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,576 **** lreplace:;
  		switch (result)
  		{
  			case HeapTupleSelfUpdated:
! 				/* already deleted by self; nothing to do */
  				return NULL;
  
  			case HeapTupleMayBeUpdated:
--- 593,617 ----
  		switch (result)
  		{
  			case HeapTupleSelfUpdated:
! 				/*
! 				 * 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:
*** a/src/test/regress/expected/triggers.out
--- b/src/test/regress/expected/triggers.out
***************
*** 1414,1416 **** NOTICE:  drop cascades to 2 other objects
--- 1414,1511 ----
  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;
*** a/src/test/regress/sql/triggers.sql
--- b/src/test/regress/sql/triggers.sql
***************
*** 935,937 **** SELECT * FROM city_view;
--- 935,1008 ----
  
  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;