From 68de3edeedd950de9d87d14b3bbc07d4dc137288 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Thu, 30 Jul 2026 12:37:19 -0700 Subject: [PATCH v1 1/7] Obey EXEC_FLAG_SKIP_TRIGGERS when inserting temporal leftovers When we insert the temporal leftovers of an UPDATE/DELETE FOR PORTION OF, we give each insert its own AFTER trigger query level, so that it looks like the separate INSERT statement the standard says it should be. But we should not do that for queries started with EXEC_FLAG_SKIP_TRIGGERS, which asks us to leave our events for the enclosing query to fire. Nothing reaches this path yet: the only caller that passes fire_triggers = false to SPI_execute_snapshot is referential integrity, and temporal foreign keys currently support only NO ACTION, which never runs a FOR PORTION OF statement. But we want to add CASCADE/SET NULL/SET DEFAULT, so this fix allows that. Author: Paul A. Jungwirth --- src/backend/executor/nodeModifyTable.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 3056b850f73..355f681b0c8 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -1436,6 +1436,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, ReturnSetInfo rsi; bool didInit = false; bool shouldFree = false; + bool fire_triggers = !(estate->es_top_eflags & EXEC_FLAG_SKIP_TRIGGERS); ResultRelInfo *rootRelInfo = mtstate->rootResultRelInfo; bool partitionRouting = rootRelInfo && @@ -1610,15 +1611,30 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, * its own transition table. If we just push & pop a new trigger level * for each insert, we get exactly what we need. * + * But we also must obey EXEC_FLAG_SKIP_TRIGGERS. Our own temporal + * foreign keys use FOR PORTION OF to implement CASCADE/SET NULL/SET + * DEFAULT, which may insert temporal leftovers on the referencing + * table, causing its INSERT triggers to fire. If they had their own + * trigger level, they would fire before the temporal foreign key + * finished its work, observing an invalid intermediate state. In + * particular we would get spurious foreign key failures: if the + * original ON UPDATE CASCADE foreign key action deleted two parts of + * history, then inserting leftovers from the first delete would fail, + * because its reference has disappeared. (Note that despite the name, + * EXEC_FLAG_SKIP_TRIGGERS doesn't *skip* the triggers, only change + * when they fire.) + * * We have to make sure that the inserts don't add to the ROW_COUNT * diagnostic or the command tag, so we pass false for canSetTag. */ - AfterTriggerBeginQuery(); + if (fire_triggers) + AfterTriggerBeginQuery(); ExecSetupTransitionCaptureState(mtstate, estate); fireBSTriggers(mtstate); ExecInsert(context, resultRelInfo, leftoverSlot, false, NULL, NULL); fireASTriggers(mtstate); - AfterTriggerEndQuery(estate); + if (fire_triggers) + AfterTriggerEndQuery(estate); } if (didInit) -- 2.45.0