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:
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;
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:
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;
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;