From 855308644564531284f0e2979fe00dc0427999a0 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Fri, 13 Jun 2025 15:40:06 -0700 Subject: [PATCH v1 2/7] Add tg_temporal to TriggerData This needs to be passed to our RI triggers to implement temporal CASCADE/SET NULL/SET DEFAULT when the user command is an UPDATE/DELETE FOR PORTION OF. The triggers will use the FOR PORTION OF bounds to avoid over-applying the change to referencing records. Probably it is useful for user-defined triggers as well, for example auditing or trigger-based replication. We also set the field when inserting temporal leftovers, so that triggers can distinguish between a regular INSERT and an automatic INSERT just to preserve history. Author: Paul A. Jungwirth --- doc/src/sgml/trigger.sgml | 67 +++++++++++++++--- src/backend/commands/trigger.c | 94 +++++++++++++++++++++++++- src/backend/executor/nodeModifyTable.c | 2 + src/backend/parser/analyze.c | 1 + src/include/commands/trigger.h | 1 + src/include/nodes/execnodes.h | 1 + src/include/nodes/primnodes.h | 1 + 7 files changed, 155 insertions(+), 12 deletions(-) diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index 8a5e7278212..4cecdc2b9e7 100644 --- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml @@ -566,17 +566,18 @@ CALLED_AS_TRIGGER(fcinfo) typedef struct TriggerData { - NodeTag type; - TriggerEvent tg_event; - Relation tg_relation; - HeapTuple tg_trigtuple; - HeapTuple tg_newtuple; - Trigger *tg_trigger; - TupleTableSlot *tg_trigslot; - TupleTableSlot *tg_newslot; - Tuplestorestate *tg_oldtable; - Tuplestorestate *tg_newtable; - const Bitmapset *tg_updatedcols; + NodeTag type; + TriggerEvent tg_event; + Relation tg_relation; + HeapTuple tg_trigtuple; + HeapTuple tg_newtuple; + Trigger *tg_trigger; + TupleTableSlot *tg_trigslot; + TupleTableSlot *tg_newslot; + Tuplestorestate *tg_oldtable; + Tuplestorestate *tg_newtable; + const Bitmapset *tg_updatedcols; + ForPortionOfState *tg_temporal; } TriggerData; @@ -844,6 +845,50 @@ typedef struct Trigger + + + tg_temporal + + + Set for UPDATE and DELETE queries + that use FOR PORTION OF, otherwise NULL. + Also set for the implicit INSERT statements to add + the temporal + leftovers. Contains a pointer to a structure of type + ForPortionOfState, defined in + nodes/execnodes.h: + + +typedef struct ForPortionOfState +{ + NodeTag type; + + Oid fp_rangeType; /* the type of the FOR PORTION OF expression */ + int fp_rangeAttno; /* the attno of the range/multirange column */ + Datum fp_targetRange; /* the range/multirange from FOR PORTION OF */ + TypeCacheEntry *fp_leftoverstypcache; /* type cache entry of the range/multirange */ + TupleTableSlot *fp_Existing; /* slot to store old tuple */ + TupleTableSlot *fp_Leftover; /* slot to store leftover */ + char *fp_rangeName; /* the column named in FOR PORTION OF */ +} ForPortionOfState; + + + where fp_rangeType is its range type, + fp_rangeAttno is its attribute number, + fp_targetRange is a rangetype value created + by evaluating the FOR PORTION OF bounds, + fp_leftoverstypcache is the type cache entry of the range column, + and fp_rangeName is the range + column named in the FOR PORTION OF clause. + The fp_Existing and + fp_Leftover tuple table slots are for the + executor's own use, and trigger functions should not rely on them: they + do not outlive the query that created them, so they are + NULL in an AFTER trigger, which may + run much later. + + + diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 79ffd2cada6..289fbf774e4 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -49,6 +49,7 @@ #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/datum.h" #include "utils/fmgroids.h" #include "utils/guc_hooks.h" #include "utils/inval.h" @@ -2459,6 +2460,7 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_event = TRIGGER_EVENT_INSERT | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2515,6 +2517,7 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2619,6 +2622,7 @@ ExecIRInsertTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2688,6 +2692,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_event = TRIGGER_EVENT_DELETE | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2796,6 +2801,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { HeapTuple newtuple; @@ -2897,6 +2903,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; ExecForceStoreHeapTuple(trigtuple, slot, false); @@ -2960,6 +2967,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_updatedcols = updatedCols; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -3103,6 +3111,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; updatedCols = ExecGetAllUpdatedCols(relinfo, estate); LocTriggerData.tg_updatedcols = updatedCols; for (i = 0; i < trigdesc->numtriggers; i++) @@ -3265,6 +3274,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; ExecForceStoreHeapTuple(trigtuple, oldslot, false); @@ -3736,6 +3746,7 @@ typedef struct AfterTriggerSharedData Oid ats_relid; /* the relation it's on */ Oid ats_rolid; /* role to execute the trigger */ CommandId ats_firing_id; /* ID for firing cycle */ + ForPortionOfState *ats_for_portion_of; /* the FOR PORTION OF clause */ struct AfterTriggersTableData *ats_table; /* transition table access */ Bitmapset *ats_modifiedcols; /* modified columns */ } AfterTriggerSharedData; @@ -4128,6 +4139,81 @@ afterTriggerCopyBitmap(Bitmapset *src) return dst; } +/* ---------- + * afterTriggerCopyForPortionOfState() + * + * Copies a ForPortionOfState into the afterTriggers memory context, so that + * it survives long enough for DEFERRED triggers. + * + * We copy only what a trigger can use: the range column's name, type and + * attribute number, and the bounds computed for the clause. The tuple table + * slots are left NULL, since they belong to the executor and are gone by the + * time a deferred trigger runs. The type cache entry is safe to share, + * because those live as long as the session. + */ +static ForPortionOfState * +afterTriggerCopyForPortionOfState(ForPortionOfState *src) +{ + ForPortionOfState *dst; + MemoryContext oldcxt; + bool typbyval; + int16 typlen; + + if (src == NULL) + return NULL; + + oldcxt = MemoryContextSwitchTo(afterTriggers.event_cxt); + + get_typlenbyval(src->fp_rangeType, &typlen, &typbyval); + + dst = makeNode(ForPortionOfState); + dst->fp_rangeName = pstrdup(src->fp_rangeName); + dst->fp_rangeType = src->fp_rangeType; + dst->fp_rangeAttno = src->fp_rangeAttno; + dst->fp_leftoverstypcache = src->fp_leftoverstypcache; + dst->fp_Existing = NULL; + dst->fp_Leftover = NULL; + dst->fp_targetRange = datumCopy(src->fp_targetRange, typbyval, typlen); + + MemoryContextSwitchTo(oldcxt); + + return dst; +} + +/* ---------- + * afterTriggerForPortionOfStatesEqual() + * + * Tells whether two ForPortionOfStates describe the same FOR PORTION OF + * clause, so that events can share one AfterTriggerSharedData. + * + * We must compare by value, not by pointer: what a shared record holds is a + * copy made by afterTriggerCopyForPortionOfState, so it never has the same + * address as the state the executor is handing us. This is the same reason + * we compare ats_modifiedcols with bms_equal. + */ +static bool +afterTriggerForPortionOfStatesEqual(ForPortionOfState *a, ForPortionOfState *b) +{ + bool typbyval; + int16 typlen; + + if (a == NULL || b == NULL) + return a == b; + + if (a->fp_rangeType != b->fp_rangeType || + a->fp_rangeAttno != b->fp_rangeAttno) + return false; + + /* + * The bounds are computed once per query, so the two datums are byte for + * byte identical whenever they came from the same clause, and + * datumIsEqual is enough. (They are freshly built range values, never + * toasted.) + */ + get_typlenbyval(a->fp_rangeType, &typlen, &typbyval); + return datumIsEqual(a->fp_targetRange, b->fp_targetRange, typbyval, typlen); +} + /* ---------- * afterTriggerAddEvent() * @@ -4232,7 +4318,9 @@ afterTriggerAddEvent(AfterTriggerEventList *events, newshared->ats_relid == evtshared->ats_relid && newshared->ats_rolid == evtshared->ats_rolid && bms_equal(newshared->ats_modifiedcols, - evtshared->ats_modifiedcols)) + evtshared->ats_modifiedcols) && + afterTriggerForPortionOfStatesEqual(newshared->ats_for_portion_of, + evtshared->ats_for_portion_of)) break; } if ((char *) newshared >= chunk->endptr) @@ -4241,6 +4329,8 @@ afterTriggerAddEvent(AfterTriggerEventList *events, *newshared = *evtshared; /* now we must make a suitably-long-lived copy of the bitmap */ newshared->ats_modifiedcols = afterTriggerCopyBitmap(evtshared->ats_modifiedcols); + /* Likewise we must make a copy of the ForPortionOfState */ + newshared->ats_for_portion_of = afterTriggerCopyForPortionOfState(evtshared->ats_for_portion_of); newshared->ats_firing_id = 0; /* just to be sure */ chunk->endfree = (char *) newshared; } @@ -4597,6 +4687,7 @@ AfterTriggerExecute(EState *estate, LocTriggerData.tg_event = evtshared->ats_event & (TRIGGER_EVENT_OPMASK | TRIGGER_EVENT_ROW); LocTriggerData.tg_relation = rel; + LocTriggerData.tg_temporal = evtshared->ats_for_portion_of; if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype)) LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols; @@ -6695,6 +6786,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, else new_shared.ats_table = NULL; new_shared.ats_modifiedcols = modifiedCols; + new_shared.ats_for_portion_of = relinfo->ri_forPortionOf; afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events, &new_event, &new_shared); diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 355f681b0c8..ac9b6b7bdf9 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -5668,6 +5668,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) /* Create state for FOR PORTION OF operation */ fpoState = makeNode(ForPortionOfState); + fpoState->fp_rangeName = pstrdup(forPortionOf->range_name); fpoState->fp_rangeType = forPortionOf->rangeType; fpoState->fp_rangeAttno = forPortionOf->rangeVar->varattno; fpoState->fp_targetRange = targetRange; @@ -5954,6 +5955,7 @@ ExecInitForPortionOf(ModifyTableState *mtstate, EState *estate, leafState = makeNode(ForPortionOfState); + leafState->fp_rangeName = pstrdup(fpoState->fp_rangeName); leafState->fp_rangeType = fpoState->fp_rangeType; leafState->fp_targetRange = fpoState->fp_targetRange; map = ExecGetChildToRootMap(resultRelInfo); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 263d1b6e1cc..0be92fb7013 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1608,6 +1608,7 @@ transformForPortionOfClause(ParseState *pstate, result->location = forPortionOf->location; result->targetLocation = forPortionOf->target_location; + result->range_name = forPortionOf->range_name; return result; } diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index fecdb785f35..97ce13e13f8 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -41,6 +41,7 @@ typedef struct TriggerData Tuplestorestate *tg_oldtable; Tuplestorestate *tg_newtable; const Bitmapset *tg_updatedcols; + ForPortionOfState *tg_temporal; } TriggerData; /* diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index f0cb21444b2..524f9beef50 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -483,6 +483,7 @@ typedef struct ForPortionOfState TypeCacheEntry *fp_leftoverstypcache; /* type cache entry of the range */ TupleTableSlot *fp_Existing; /* slot to store old tuple */ TupleTableSlot *fp_Leftover; /* slot to store leftover */ + char *fp_rangeName; /* the column named in FOR PORTION OF */ } ForPortionOfState; /* diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 5a636d1f179..e4d6e78e621 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -2453,6 +2453,7 @@ typedef struct ForPortionOfExpr Oid withoutPortionProc; /* SRF proc for old_range - target_range */ ParseLoc location; /* token location, or -1 if unknown */ ParseLoc targetLocation; /* token location, or -1 if unknown */ + char *range_name; /* Range name */ } ForPortionOfExpr; #endif /* PRIMNODES_H */ -- 2.45.0