From d8317f4e967223b929fd4e327643dca0f8ad4bf8 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v1 1/5] 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 <pj@illuminatedcomputing.com>
---
 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)
 <programlisting>
 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;
 </programlisting>
 
@@ -844,6 +845,50 @@ typedef struct Trigger
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry>
+      <term><structfield>tg_temporal</structfield></term>
+      <listitem>
+       <para>
+        Set for <literal>UPDATE</literal> and <literal>DELETE</literal> queries
+        that use <literal>FOR PORTION OF</literal>, otherwise <symbol>NULL</symbol>.
+        Also set for the implicit <literal>INSERT</literal> statements to add
+        the <glossterm linkend="glossary-temporal-leftovers">temporal
+        leftovers</glossterm>. Contains a pointer to a structure of type
+        <structname>ForPortionOfState</structname>, defined in
+        <filename>nodes/execnodes.h</filename>:
+
+<programlisting>
+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;
+</programlisting>
+
+       where <structfield>fp_rangeType</structfield> is its range type,
+       <structfield>fp_rangeAttno</structfield> is its attribute number,
+       <structfield>fp_targetRange</structfield> is a rangetype value created
+       by evaluating the <literal>FOR PORTION OF</literal> bounds,
+       <structfield>fp_leftoverstypcache</structfield> is the type cache entry of the range column,
+       and <structfield>fp_rangeName</structfield> is the range
+       column named in the <literal>FOR PORTION OF</literal> clause.
+       The <structfield>fp_Existing</structfield> and
+       <structfield>fp_Leftover</structfield> 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
+       <symbol>NULL</symbol> in an <literal>AFTER</literal> trigger, which may
+       run much later.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
 
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 3056b850f73..b272c5a9733 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -5652,6 +5652,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;
@@ -5938,6 +5939,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.47.3

