From 589bdaeb98c4529ff68c2e62183685f8fb909406 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Mon, 9 Nov 2026 21:14:47 -0500 Subject: [PATCH v2 REL_17-19] Fix transition tables for cross-partition MERGE updates A MERGE UPDATE that moves a row executes a physical DELETE and INSERT. Capture both halves in the UPDATE transition tables and exclude them from the DELETE and INSERT transition tables, even when the UPDATE transition tables are not requested. Reported-by: Michael Ries Author: Tender Wang Reviewed-by: Shihao Zhong Discussion: https://postgr.es/m/CAKWDHLgETF86qEW40HcEzMgn5DdmS6yR-nSYwemXF7zFGrU+sQ@mail.gmail.com Discussion: https://postgr.es/m/CAHewXNk8MgaFMD87s=xHeguzmErEjsqeroxrMyQbKyXwG5eocw@mail.gmail.com Backpatch-through: 15 --- src/backend/executor/nodeModifyTable.c | 50 ++++++++++++-------------- src/test/regress/expected/triggers.out | 31 ++++++++++++++++ src/test/regress/sql/triggers.sql | 30 ++++++++++++++++ 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index eb1acdf4243..34f27095218 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -1289,22 +1289,21 @@ ExecInsert(ModifyTableContext *context, if (mtstate->mt_transition_capture) mtstate->mt_transition_capture->tcs_original_insert_tuple = tcs_original_insert_tuple; ar_insert_trig_tcs = mtstate->mt_transition_capture; - if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture - && mtstate->mt_transition_capture->tcs_update_new_table) + if (mtstate->operation == CMD_UPDATE || + (mtstate->operation == CMD_MERGE && + mtstate->mt_merge_action->mas_action->commandType == CMD_UPDATE)) { - ExecARUpdateTriggers(estate, resultRelInfo, - NULL, NULL, - NULL, - NULL, - slot, - NULL, - mtstate->mt_transition_capture, - false); - - /* - * We've already captured the NEW TABLE row, so make sure any AR - * INSERT trigger fired below doesn't capture it again. - */ + if (ar_insert_trig_tcs && ar_insert_trig_tcs->tcs_update_new_table) + ExecARUpdateTriggers(estate, resultRelInfo, + NULL, NULL, + NULL, + NULL, + slot, + NULL, + mtstate->mt_transition_capture, + false); + + /* A moved row does not belong in the INSERT transition table. */ ar_insert_trig_tcs = NULL; } @@ -1575,19 +1574,16 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * because they happen on different tables. */ ar_delete_trig_tcs = mtstate->mt_transition_capture; - if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture && - mtstate->mt_transition_capture->tcs_update_old_table) + if (changingPart) { - ExecARUpdateTriggers(estate, resultRelInfo, - NULL, NULL, - tupleid, oldtuple, - NULL, NULL, mtstate->mt_transition_capture, - false); - - /* - * We've already captured the OLD TABLE row, so make sure any AR - * DELETE trigger fired below doesn't capture it again. - */ + if (ar_delete_trig_tcs && ar_delete_trig_tcs->tcs_update_old_table) + ExecARUpdateTriggers(estate, resultRelInfo, + NULL, NULL, + tupleid, oldtuple, + NULL, NULL, mtstate->mt_transition_capture, + false); + + /* A moved row does not belong in the DELETE transition table. */ ar_delete_trig_tcs = NULL; } diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 03d9562ec90..16e71341766 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -3407,6 +3407,37 @@ select * from merge_conv_table order by a; (2 rows) drop table merge_conv_table; +-- A MERGE UPDATE that moves a row must capture it as an update. +create table merge_move (a int, b text) partition by list (a); +create table merge_move_p1 partition of merge_move for values in (1, 2); +create table merge_move_p2 partition of merge_move for values in (3, 4); +insert into merge_move values (1, 'move'), (2, 'delete'); +create trigger move_ins after insert on merge_move + referencing new table as new_table + for each statement execute function dump_insert(); +create trigger move_upd after update on merge_move + referencing old table as old_table new table as new_table + for each statement execute function dump_update(); +create trigger move_del after delete on merge_move + referencing old table as old_table + for each statement execute function dump_delete(); +merge into merge_move t using (values (1), (2), (4)) s(a) on t.a = s.a +when matched and s.a = 1 then update set a = 3 +when matched then delete +when not matched then insert values (s.a, 'insert'); +NOTICE: trigger = move_del, old table = (2,delete) +NOTICE: trigger = move_upd, old table = (1,move), new table = (3,move) +NOTICE: trigger = move_ins, new table = (4,insert) +-- The move must stay out of INSERT and DELETE tables even without an +-- UPDATE transition table. +drop trigger move_upd on merge_move; +merge into merge_move t using (values (3), (4), (2)) s(a) on t.a = s.a +when matched and s.a = 3 then update set a = 1 +when matched then delete +when not matched then insert values (s.a, 'insert'); +NOTICE: trigger = move_del, old table = (4,insert) +NOTICE: trigger = move_ins, new table = (2,insert) +drop table merge_move; -- cleanup drop function dump_insert(); drop function dump_update(); diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 71458ec2d66..99e85fac394 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -2568,6 +2568,36 @@ select * from merge_conv_table order by a; drop table merge_conv_table; +-- A MERGE UPDATE that moves a row must capture it as an update. +create table merge_move (a int, b text) partition by list (a); +create table merge_move_p1 partition of merge_move for values in (1, 2); +create table merge_move_p2 partition of merge_move for values in (3, 4); +insert into merge_move values (1, 'move'), (2, 'delete'); +create trigger move_ins after insert on merge_move + referencing new table as new_table + for each statement execute function dump_insert(); +create trigger move_upd after update on merge_move + referencing old table as old_table new table as new_table + for each statement execute function dump_update(); +create trigger move_del after delete on merge_move + referencing old table as old_table + for each statement execute function dump_delete(); + +merge into merge_move t using (values (1), (2), (4)) s(a) on t.a = s.a +when matched and s.a = 1 then update set a = 3 +when matched then delete +when not matched then insert values (s.a, 'insert'); + +-- The move must stay out of INSERT and DELETE tables even without an +-- UPDATE transition table. +drop trigger move_upd on merge_move; +merge into merge_move t using (values (3), (4), (2)) s(a) on t.a = s.a +when matched and s.a = 3 then update set a = 1 +when matched then delete +when not matched then insert values (s.a, 'insert'); + +drop table merge_move; + -- cleanup drop function dump_insert(); drop function dump_update(); -- 2.37.1 (Apple Git-137.1)