Fix AFTER ROW trigger execution in MERGE cross-partition update.
authorDean Rasheed <[email protected]>
Thu, 9 Nov 2023 11:23:42 +0000 (11:23 +0000)
committerDean Rasheed <[email protected]>
Thu, 9 Nov 2023 11:23:42 +0000 (11:23 +0000)
When executing a MERGE UPDATE action, if the UPDATE is turned into a
cross-partition DELETE then INSERT, do not attempt to invoke AFTER
UPDATE ROW triggers, or any of the other post-update actions in
ExecUpdateEpilogue().

For consistency with a plain UPDATE command, such triggers should not
be fired (and typically fail anyway), and similarly, other post-update
actions, such as WCO/RLS checks should not be executed, and might also
lead to unexpected failures.

Therefore, as with ExecUpdate(), make ExecMergeMatched() return
immediately if ExecUpdateAct() reports that a cross-partition update
was done, to be sure that no further processing is done for that
tuple.

Back-patch to v15, where MERGE was introduced.

Discussion: https://postgr.es/m/CAEZATCWjBgagyNZs02vgDF0DvASYj-iHTFtXG2-nP3orZhmtcw%40mail.gmail.com

src/backend/executor/nodeModifyTable.c
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index 299c2c75be8f596079bc63618ad59b677036a395..b16fbe9e22a2db9f24c4b9dbc0ba17a8391bb07d 100644 (file)
@@ -2899,6 +2899,22 @@ lmerge_matched:
                                }
                                result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
                                                                           newslot, false, &updateCxt);
+
+                               /*
+                                * As in ExecUpdate(), if ExecUpdateAct() reports that a
+                                * cross-partition update was done, then there's nothing else
+                                * for us to do --- the UPDATE has been turned into a DELETE
+                                * and an INSERT, and we must not perform any of the usual
+                                * post-update tasks.
+                                */
+                               if (updateCxt.crossPartUpdate)
+                               {
+                                       mtstate->mt_merge_updated += 1;
+                                       if (canSetTag)
+                                               (estate->es_processed)++;
+                                       return true;
+                               }
+
                                if (result == TM_Ok && updateCxt.updated)
                                {
                                        ExecUpdateEpilogue(context, &updateCxt, resultRelInfo,
index e8de916dfe2252bfe6428152c70bf9f20e3cdf45..78e90309238c05fab93c9623add9c96bb39896d7 100644 (file)
@@ -2309,6 +2309,51 @@ NOTICE:  trigger zzz on parted_trig_1_1 AFTER INSERT for ROW
 NOTICE:  trigger bbb on parted_trig_2 AFTER INSERT for ROW
 NOTICE:  trigger zzz on parted_trig_2 AFTER INSERT for ROW
 drop table parted_trig;
+-- Verify that the correct triggers fire for cross-partition updates
+create table parted_trig (a int) partition by list (a);
+create table parted_trig1 partition of parted_trig for values in (1);
+create table parted_trig2 partition of parted_trig for values in (2);
+insert into parted_trig values (1);
+create or replace function trigger_notice() returns trigger as $$
+  begin
+    raise notice 'trigger % on % % % for %', TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+    if TG_LEVEL = 'ROW' then
+      if TG_OP = 'DELETE' then
+        return OLD;
+      else
+        return NEW;
+      end if;
+    end if;
+    return null;
+  end;
+  $$ language plpgsql;
+create trigger parted_trig_before_stmt before insert or update or delete on parted_trig
+   for each statement execute procedure trigger_notice();
+create trigger parted_trig_before_row before insert or update or delete on parted_trig
+   for each row execute procedure trigger_notice();
+create trigger parted_trig_after_row after insert or update or delete on parted_trig
+   for each row execute procedure trigger_notice();
+create trigger parted_trig_after_stmt after insert or update or delete on parted_trig
+   for each statement execute procedure trigger_notice();
+update parted_trig set a = 2 where a = 1;
+NOTICE:  trigger parted_trig_before_stmt on parted_trig BEFORE UPDATE for STATEMENT
+NOTICE:  trigger parted_trig_before_row on parted_trig1 BEFORE UPDATE for ROW
+NOTICE:  trigger parted_trig_before_row on parted_trig1 BEFORE DELETE for ROW
+NOTICE:  trigger parted_trig_before_row on parted_trig2 BEFORE INSERT for ROW
+NOTICE:  trigger parted_trig_after_row on parted_trig1 AFTER DELETE for ROW
+NOTICE:  trigger parted_trig_after_row on parted_trig2 AFTER INSERT for ROW
+NOTICE:  trigger parted_trig_after_stmt on parted_trig AFTER UPDATE for STATEMENT
+-- update action in merge should behave the same
+merge into parted_trig using (select 1) as ss on true
+  when matched and a = 2 then update set a = 1;
+NOTICE:  trigger parted_trig_before_stmt on parted_trig BEFORE UPDATE for STATEMENT
+NOTICE:  trigger parted_trig_before_row on parted_trig2 BEFORE UPDATE for ROW
+NOTICE:  trigger parted_trig_before_row on parted_trig2 BEFORE DELETE for ROW
+NOTICE:  trigger parted_trig_before_row on parted_trig1 BEFORE INSERT for ROW
+NOTICE:  trigger parted_trig_after_row on parted_trig2 AFTER DELETE for ROW
+NOTICE:  trigger parted_trig_after_row on parted_trig1 AFTER INSERT for ROW
+NOTICE:  trigger parted_trig_after_stmt on parted_trig AFTER UPDATE for STATEMENT
+drop table parted_trig;
 -- Verify propagation of trigger arguments to partitions
 create table parted_trig (a int) partition by list (a);
 create table parted_trig1 partition of parted_trig for values in (1);
index d29e98d2ac944007d9eb4f3032ce9efb8094fde7..46795a9c789e91bab543be5d37a6520601c9a9bd 100644 (file)
@@ -1583,6 +1583,42 @@ create trigger qqq after insert on parted_trig_1_1 for each row execute procedur
 insert into parted_trig values (50), (1500);
 drop table parted_trig;
 
+-- Verify that the correct triggers fire for cross-partition updates
+create table parted_trig (a int) partition by list (a);
+create table parted_trig1 partition of parted_trig for values in (1);
+create table parted_trig2 partition of parted_trig for values in (2);
+insert into parted_trig values (1);
+
+create or replace function trigger_notice() returns trigger as $$
+  begin
+    raise notice 'trigger % on % % % for %', TG_NAME, TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+    if TG_LEVEL = 'ROW' then
+      if TG_OP = 'DELETE' then
+        return OLD;
+      else
+        return NEW;
+      end if;
+    end if;
+    return null;
+  end;
+  $$ language plpgsql;
+create trigger parted_trig_before_stmt before insert or update or delete on parted_trig
+   for each statement execute procedure trigger_notice();
+create trigger parted_trig_before_row before insert or update or delete on parted_trig
+   for each row execute procedure trigger_notice();
+create trigger parted_trig_after_row after insert or update or delete on parted_trig
+   for each row execute procedure trigger_notice();
+create trigger parted_trig_after_stmt after insert or update or delete on parted_trig
+   for each statement execute procedure trigger_notice();
+
+update parted_trig set a = 2 where a = 1;
+
+-- update action in merge should behave the same
+merge into parted_trig using (select 1) as ss on true
+  when matched and a = 2 then update set a = 1;
+
+drop table parted_trig;
+
 -- Verify propagation of trigger arguments to partitions
 create table parted_trig (a int) partition by list (a);
 create table parted_trig1 partition of parted_trig for values in (1);