Fix MakeTransitionCaptureState() to return a consistent result
authorMichael Paquier <[email protected]>
Thu, 13 Feb 2025 07:31:12 +0000 (16:31 +0900)
committerMichael Paquier <[email protected]>
Thu, 13 Feb 2025 07:31:12 +0000 (16:31 +0900)
When an UPDATE trigger referencing a new table and a DELETE trigger
referencing an old table are both present, MakeTransitionCaptureState()
returns an inconsistent result for UPDATE commands in its set of flags
and tuplestores holding the TransitionCaptureState for transition
tables.

As proved by the test added here, this issue causes a crash in v14 and
earlier versions (down to 11, actually, older versions do not support
triggers on partitioned tables) during cross-partition updates on a
partitioned table.  v15 and newer versions are safe thanks to
7103ebb7aae8.

This commit fixes the function so that it returns a consistent state
by using portions of the changes made in commit 7103ebb7aae8 for v13 and
v14.  v15 and newer versions are slightly tweaked to match with the
older versions, mainly for consistency across branches.

Author: Kyotaro Horiguchi
Discussion: https://postgr.es/m/20250207.150238.968446820828052276[email protected]
Backpatch-through: 13

src/backend/commands/trigger.c
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index 2ee4d0720ac2496049c93102898f655fa399a7f3..8ecd9df207828cf30ed4e1bddc33565aa8d00f8b 100644 (file)
@@ -4413,8 +4413,10 @@ TransitionCaptureState *
 MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
 {
    TransitionCaptureState *state;
-   bool        need_old,
-               need_new;
+   bool        need_old_upd,
+               need_new_upd,
+               need_old_del,
+               need_new_ins;
    AfterTriggersTableData *table;
    MemoryContext oldcxt;
    ResourceOwner saveResourceOwner;
@@ -4426,23 +4428,25 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
    switch (cmdType)
    {
        case CMD_INSERT:
-           need_old = false;
-           need_new = trigdesc->trig_insert_new_table;
+           need_old_upd = need_old_del = need_new_upd = false;
+           need_new_ins = trigdesc->trig_insert_new_table;
            break;
        case CMD_UPDATE:
-           need_old = trigdesc->trig_update_old_table;
-           need_new = trigdesc->trig_update_new_table;
+           need_old_upd = trigdesc->trig_update_old_table;
+           need_new_upd = trigdesc->trig_update_new_table;
+           need_old_del = need_new_ins = false;
            break;
        case CMD_DELETE:
-           need_old = trigdesc->trig_delete_old_table;
-           need_new = false;
+           need_old_del = trigdesc->trig_delete_old_table;
+           need_old_upd = need_new_upd = need_new_ins = false;
            break;
        default:
            elog(ERROR, "unexpected CmdType: %d", (int) cmdType);
-           need_old = need_new = false;    /* keep compiler quiet */
+           /* keep compiler quiet */
+           need_old_upd = need_new_upd = need_old_del = need_new_ins = false;
            break;
    }
-   if (!need_old && !need_new)
+   if (!need_old_upd && !need_new_upd && !need_new_ins && !need_old_del)
        return NULL;
 
    /* Check state, like AfterTriggerSaveEvent. */
@@ -4472,9 +4476,9 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
    saveResourceOwner = CurrentResourceOwner;
    CurrentResourceOwner = CurTransactionResourceOwner;
 
-   if (need_old && table->old_tuplestore == NULL)
+   if ((need_old_upd || need_old_del) && table->old_tuplestore == NULL)
        table->old_tuplestore = tuplestore_begin_heap(false, false, work_mem);
-   if (need_new && table->new_tuplestore == NULL)
+   if ((need_new_upd || need_new_ins) && table->new_tuplestore == NULL)
        table->new_tuplestore = tuplestore_begin_heap(false, false, work_mem);
 
    CurrentResourceOwner = saveResourceOwner;
@@ -4482,10 +4486,10 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
 
    /* Now build the TransitionCaptureState struct, in caller's context */
    state = (TransitionCaptureState *) palloc0(sizeof(TransitionCaptureState));
-   state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
-   state->tcs_update_old_table = trigdesc->trig_update_old_table;
-   state->tcs_update_new_table = trigdesc->trig_update_new_table;
-   state->tcs_insert_new_table = trigdesc->trig_insert_new_table;
+   state->tcs_delete_old_table = need_old_del;
+   state->tcs_update_old_table = need_old_upd;
+   state->tcs_update_new_table = need_new_upd;
+   state->tcs_insert_new_table = need_new_ins;
    state->tcs_private = table;
 
    return state;
index 0e027decbb465b4ce9c748f21ff69246bb2cdc3f..a321105648114e20e576385cdcb3d82c2845ae63 100644 (file)
@@ -2988,6 +2988,55 @@ drop trigger child_row_trig on child;
 alter table parent attach partition child for values in ('AAA');
 drop table child, parent;
 --
+-- Verify access of transition tables with UPDATE triggers and tuples
+-- moved across partitions.
+--
+create or replace function dump_update_new() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %', TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+create or replace function dump_update_old() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %', TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_table);
+    return null;
+  end;
+$$;
+create table trans_tab_parent (a text) partition by list (a);
+create table trans_tab_child1 partition of trans_tab_parent for values in ('AAA1', 'AAA2');
+create table trans_tab_child2 partition of trans_tab_parent for values in ('BBB1', 'BBB2');
+create trigger trans_tab_parent_update_trig
+  after update on trans_tab_parent referencing old table as old_table
+  for each statement execute procedure dump_update_old();
+create trigger trans_tab_parent_insert_trig
+  after insert on trans_tab_parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger trans_tab_parent_delete_trig
+  after delete on trans_tab_parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+insert into trans_tab_parent values ('AAA1'), ('BBB1');
+NOTICE:  trigger = trans_tab_parent_insert_trig, new table = (AAA1), (BBB1)
+-- should not trigger access to new table when moving across partitions.
+update trans_tab_parent set a = 'BBB2' where a = 'AAA1';
+NOTICE:  trigger = trans_tab_parent_update_trig, old table = (AAA1)
+drop trigger trans_tab_parent_update_trig on trans_tab_parent;
+create trigger trans_tab_parent_update_trig
+  after update on trans_tab_parent referencing new table as new_table
+  for each statement execute procedure dump_update_new();
+-- should not trigger access to old table when moving across partitions.
+update trans_tab_parent set a = 'AAA2' where a = 'BBB1';
+NOTICE:  trigger = trans_tab_parent_update_trig, new table = (AAA2)
+delete from trans_tab_parent;
+NOTICE:  trigger = trans_tab_parent_delete_trig, old table = (AAA2), (BBB2)
+-- clean up
+drop table trans_tab_parent, trans_tab_child1, trans_tab_child2;
+drop function dump_update_new, dump_update_old;
+--
 -- Verify behavior of statement triggers on (non-partition)
 -- inheritance hierarchy with transition tables; similar to the
 -- partition case, except there is no rerouting on insertion and child
index 605d5cf05eed9a7db1753253266f319d113779fe..e12a8d27023ba49a95a787bb04f4c8355a3a0d00 100644 (file)
@@ -2115,6 +2115,52 @@ alter table parent attach partition child for values in ('AAA');
 
 drop table child, parent;
 
+--
+-- Verify access of transition tables with UPDATE triggers and tuples
+-- moved across partitions.
+--
+create or replace function dump_update_new() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %', TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+create or replace function dump_update_old() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %', TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_table);
+    return null;
+  end;
+$$;
+create table trans_tab_parent (a text) partition by list (a);
+create table trans_tab_child1 partition of trans_tab_parent for values in ('AAA1', 'AAA2');
+create table trans_tab_child2 partition of trans_tab_parent for values in ('BBB1', 'BBB2');
+create trigger trans_tab_parent_update_trig
+  after update on trans_tab_parent referencing old table as old_table
+  for each statement execute procedure dump_update_old();
+create trigger trans_tab_parent_insert_trig
+  after insert on trans_tab_parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger trans_tab_parent_delete_trig
+  after delete on trans_tab_parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+insert into trans_tab_parent values ('AAA1'), ('BBB1');
+-- should not trigger access to new table when moving across partitions.
+update trans_tab_parent set a = 'BBB2' where a = 'AAA1';
+drop trigger trans_tab_parent_update_trig on trans_tab_parent;
+create trigger trans_tab_parent_update_trig
+  after update on trans_tab_parent referencing new table as new_table
+  for each statement execute procedure dump_update_new();
+-- should not trigger access to old table when moving across partitions.
+update trans_tab_parent set a = 'AAA2' where a = 'BBB1';
+delete from trans_tab_parent;
+-- clean up
+drop table trans_tab_parent, trans_tab_child1, trans_tab_child2;
+drop function dump_update_new, dump_update_old;
+
 --
 -- Verify behavior of statement triggers on (non-partition)
 -- inheritance hierarchy with transition tables; similar to the