From 3242b820625e103ba3f3f195890171eb42db2101 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Thu, 30 Jul 2026 12:46:50 -0700
Subject: [PATCH v1 6/7] Add CASCADE for temporal foreign keys

Previously we raised an error for this option, because its implementation
requires FOR PORTION OF. Now that we have temporal UPDATE/DELETE, we can
implement foreign key actions that need it.

To apply a temporal CASCADE, we should UPDATE/DELETE only as much history as
changed in the referenced row. There are several scenarios to consider:

1. The user UPDATEs/DELETEs with FOR PORTION OF. Then the changed
   history is the intersection of the targeted history and the row's
   history. Any history outside of the FOR PORTION OF target is being
   preserved by temporal leftovers, so the referencing history shouldn't
   change either.

2. The user DELETEs without FOR PORTION OF. Then the row's entire history has
   changed, and we should target its full history when we apply the action to
   referencing rows.

3. The user UPDATEs without FOR PORTION OF and doesn't set any part of the
   referenced key (scalar parts or PERIOD). There is nothing to do. This is the
   same behavior as non-PERIOD foreign keys.

4. The user UPDATEs without FOR PORTION OF and SETs the scalar part of the
   referenced key and/or the PERIOD part. The action should preserve
   referential integrity and only touch references to the altered history.
   For any history that was lost, we execute a DELETE FOR PORTION OF (possibly
   two, if spans at both ends of the old history were lost). For any history
   that was kept, we execute an UPDATE FOR PORTION OF to cascade the change (if
   any) to the scalar key part.

In all cases (except 3), the triggered UPDATEs/DELETEs just need their own
FOR PORTION OF clause to target the right history. Only the FROM/TO bounds
vary between the cases.

Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
---
 doc/src/sgml/ddl.sgml                         |   4 +-
 doc/src/sgml/ref/create_table.sgml            |   7 +-
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/utils/adt/ri_triggers.c           | 331 +++++++++
 .../regress/expected/without_overlaps.out     | 636 +++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 372 +++++++++-
 src/tools/pgindent/typedefs.list              |   2 +
 7 files changed, 1336 insertions(+), 22 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 8fe6a340277..bc7d3f0220c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1861,8 +1861,8 @@ CREATE TABLE variants (
 
     <para>
      <productname>PostgreSQL</productname> supports temporal foreign keys with
-     action <literal>NO ACTION</literal> and <literal>RESTRICT</literal>, but
-     not <literal>CASCADE</literal>, <literal>SET NULL</literal>, or
+     action <literal>NO ACTION</literal>, <literal>RESTRICT</literal>, and
+     <literal>CASCADE</literal>, but not <literal>SET NULL</literal> or
      <literal>SET DEFAULT</literal>.
     </para>
    </sect3>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 9c73850e652..1805b503089 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1341,7 +1341,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
-          In a temporal foreign key, this option is not supported.
+          In a temporal foreign key, the delete/update will use
+          <literal>FOR PORTION OF</literal> semantics to constrain the
+          effect to the bounds being deleted/updated in the referenced row.
+          If an update shortens the referenced row's application time, then
+          the referencing rows are deleted for the time that was lost, and
+          the new key values are cascaded to the time that was kept.
          </para>
         </listitem>
        </varlistentry>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fef76ed9ee3..769a3d9f043 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10374,16 +10374,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	if (fkconstraint->fk_with_period)
 	{
-		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
+		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
 			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT)
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("unsupported %s action for foreign key constraint using PERIOD",
 						   "ON UPDATE"));
 
-		if (fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
+		if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
 			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6bed1786f9b..6a8fdc042df 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -232,6 +232,14 @@ typedef struct RI_CompareHashEntry
  */
 typedef void (*tri_lost_range_callback) (Datum lostRange, void *arg);
 
+typedef struct TRI_CascadeDelContext
+{
+	const RI_ConstraintInfo *riinfo;
+	Relation	fk_rel;
+	Relation	pk_rel;
+	TupleTableSlot *oldslot;
+} TRI_CascadeDelContext;
+
 typedef struct TRI_SetContext
 {
 	const RI_ConstraintInfo *riinfo;
@@ -438,10 +446,21 @@ static Datum kept_enforced_range(const ForPortionOfState *tg_temporal,
 								 const RI_ConstraintInfo *riinfo,
 								 TupleTableSlot *oldslot,
 								 TupleTableSlot *newslot);
+static bool pk_scalar_keys_equal(const RI_ConstraintInfo *riinfo,
+								 TupleTableSlot *oldslot,
+								 TupleTableSlot *newslot);
+static void tri_cascade_del(const RI_ConstraintInfo *riinfo,
+							Relation fk_rel, Relation pk_rel,
+							TupleTableSlot *oldslot, Datum targetRange);
+static void tri_cascade_upd(const RI_ConstraintInfo *riinfo,
+							Relation fk_rel, Relation pk_rel,
+							TupleTableSlot *oldslot, TupleTableSlot *newslot,
+							Datum targetRange);
 static void tri_foreach_lost_range(const RI_ConstraintInfo *riinfo,
 								   TupleTableSlot *oldslot,
 								   TupleTableSlot *newslot,
 								   tri_lost_range_callback callback, void *arg);
+static void tri_cascade_del_lost_range(Datum lostRange, void *arg);
 static void ri_restrict_lost_range(Datum lostRange, void *arg);
 
 /*
@@ -1231,6 +1250,27 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 
 	SPI_connect();
 
+	/*
+	 * For a temporal foreign key, cascade only to the history the referenced
+	 * row actually had, trimmed by an original FOR PORTION OF if present,
+	 * using a DELETE FOR PORTION OF.
+	 */
+	if (riinfo->hasperiod)
+	{
+		Datum		targetRange;
+
+		targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo,
+											  oldslot);
+		tri_cascade_del(riinfo, fk_rel, pk_rel, oldslot, targetRange);
+
+		if (SPI_finish() != SPI_OK_FINISH)
+			elog(ERROR, "SPI_finish failed");
+
+		table_close(fk_rel, RowExclusiveLock);
+
+		return PointerGetDatum(NULL);
+	}
+
 	/* Fetch or prepare a saved plan for the cascaded delete */
 	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_CASCADE_ONDELETE);
 
@@ -1336,6 +1376,55 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 
 	SPI_connect();
 
+	/*
+	 * A temporal foreign key must only touch the history that actually
+	 * changed.
+	 */
+	if (riinfo->hasperiod)
+	{
+		/*
+		 * If the UPDATE did not use FOR PORTION OF, then we have to compute
+		 * what history was lost/changed ourselves and apply the changes by
+		 * hand. If any application time was removed, we should DELETE
+		 * references to it. If the scalar part(s) of the key changed, we
+		 * should UPDATE references to it.
+		 */
+		if (!fpo_targets_pk_range(trigdata->tg_temporal, riinfo))
+		{
+			TRI_CascadeDelContext ctx;
+
+			ctx.riinfo = riinfo;
+			ctx.fk_rel = fk_rel;
+			ctx.pk_rel = pk_rel;
+			ctx.oldslot = oldslot;
+
+			/* Delete the referencing history, one lost span at a time */
+			tri_foreach_lost_range(riinfo, oldslot, newslot,
+								   tri_cascade_del_lost_range, &ctx);
+		}
+
+		/*
+		 * Cascade the new key to the history the PK row kept.  If the scalar
+		 * part of the key didn't change, referencing rows already agree with
+		 * it there, so there is nothing to cascade.
+		 */
+		if (!pk_scalar_keys_equal(riinfo, oldslot, newslot))
+		{
+			Datum		targetRange;
+
+			targetRange = kept_enforced_range(trigdata->tg_temporal, riinfo,
+											  oldslot, newslot);
+			tri_cascade_upd(riinfo, fk_rel, pk_rel, oldslot, newslot, targetRange);
+		}
+
+		if (SPI_finish() != SPI_OK_FINISH)
+			elog(ERROR, "SPI_finish failed");
+
+		table_close(fk_rel, RowExclusiveLock);
+
+		return PointerGetDatum(NULL);
+	}
+
 	/* Fetch or prepare a saved plan for the cascaded update */
 	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_CASCADE_ONUPDATE);
 
@@ -1662,6 +1751,193 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	}
 }
 
+/*
+ * tri_cascade_del -
+ *
+ * Deletes the referencing rows' history within targetRange, with a DELETE FOR
+ * PORTION OF.  This is used by ON DELETE CASCADE for the history the
+ * referenced row had, and by ON UPDATE CASCADE for the history it lost.
+ *
+ * The caller must have connected to SPI already.
+ */
+static void
+tri_cascade_del(const RI_ConstraintInfo *riinfo, Relation fk_rel, Relation pk_rel,
+				TupleTableSlot *oldslot, Datum targetRange)
+{
+	RI_QueryKey qkey;
+	SPIPlanPtr	qplan;
+
+	/* Fetch or prepare a saved plan for the cascaded delete */
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_CASCADE_ONDELETE);
+
+	if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+	{
+		StringInfoData querybuf;
+		char		fkrelname[MAX_QUOTED_REL_NAME_LEN];
+		char		attname[MAX_QUOTED_NAME_LEN];
+		char		paramname[16];
+		const char *querysep;
+		Oid			queryoids[RI_MAX_NUMKEYS + 1];
+		const char *fk_only;
+
+		/* ----------
+		 * The query string built is
+		 *  DELETE FROM [ONLY] <fktable>
+		 *  FOR PORTION OF $fkatt (${n+1})
+		 *  WHERE $1 = fkatt1 [AND ...]
+		 * The type id's for the $ parameters are those of the
+		 * corresponding PK attributes.
+		 * ----------
+		 */
+		initStringInfo(&querybuf);
+		fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+			"" : "ONLY ";
+		quoteRelationName(fkrelname, fk_rel);
+		quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+		appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s ($%d)",
+						 fk_only, fkrelname, attname, riinfo->nkeys + 1);
+		querysep = "WHERE";
+		for (int i = 0; i < riinfo->nkeys; i++)
+		{
+			Oid			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+
+			quoteOneName(attname,
+						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
+			sprintf(paramname, "$%d", i + 1);
+			ri_GenerateQual(&querybuf, querysep,
+							paramname, pk_type,
+							riinfo->pf_eq_oprs[i],
+							attname, fk_type);
+			querysep = "AND";
+			queryoids[i] = pk_type;
+		}
+
+		/* Set a param for FOR PORTION OF TO/FROM */
+		queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+		/* Prepare and save the plan */
+		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids,
+							 &qkey, fk_rel, pk_rel);
+	}
+
+	/*
+	 * We have a plan now. Build up the arguments from the key values in the
+	 * deleted PK tuple and delete the referencing rows
+	 */
+	ri_PerformCheck(riinfo, &qkey, qplan,
+					fk_rel, pk_rel,
+					oldslot, NULL,
+					riinfo->nkeys + 1, targetRange,
+					false,
+					true,		/* must detect new rows */
+					SPI_OK_DELETE);
+}
+
+/*
+ * tri_cascade_upd -
+ *
+ * Cascades the referenced row's new key to the referencing rows' history
+ * within targetRange, with an UPDATE FOR PORTION OF.
+ *
+ * The caller must have connected to SPI already.
+ */
+static void
+tri_cascade_upd(const RI_ConstraintInfo *riinfo, Relation fk_rel, Relation pk_rel,
+				TupleTableSlot *oldslot, TupleTableSlot *newslot, Datum targetRange)
+{
+	RI_QueryKey qkey;
+	SPIPlanPtr	qplan;
+
+	/* Fetch or prepare a saved plan for the cascaded update */
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_CASCADE_ONUPDATE);
+
+	if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+	{
+		StringInfoData querybuf;
+		StringInfoData qualbuf;
+		char		fkrelname[MAX_QUOTED_REL_NAME_LEN];
+		char		attname[MAX_QUOTED_NAME_LEN];
+		char		paramname[16];
+		const char *querysep;
+		const char *qualsep;
+		Oid			queryoids[2 * RI_MAX_NUMKEYS + 1];
+		const char *fk_only;
+
+		/* ----------
+		 * The query string built is
+		 *  UPDATE [ONLY] <fktable>
+		 *		  FOR PORTION OF $fkatt (${2n+1})
+		 *		  SET fkatt1 = $1, [, ...]
+		 *		  WHERE $n = fkatt1 [AND ...]
+		 * The type id's for the $ parameters are those of the
+		 * corresponding PK attributes.  Note that we are assuming
+		 * there is an assignment cast from the PK to the FK type;
+		 * else the parser will fail.
+		 * ----------
+		 */
+		initStringInfo(&querybuf);
+		initStringInfo(&qualbuf);
+		fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+			"" : "ONLY ";
+		quoteRelationName(fkrelname, fk_rel);
+		quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+
+		appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s ($%d) SET",
+						 fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1);
+
+		querysep = "";
+		qualsep = "WHERE";
+		for (int i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++)
+		{
+			Oid			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+
+			quoteOneName(attname,
+						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
+			/*
+			 * Don't set the temporal column(s). FOR PORTION OF will take care
+			 * of that.
+			 */
+			if (i < riinfo->nkeys - 1)
+				appendStringInfo(&querybuf,
+								 "%s %s = $%d",
+								 querysep, attname, i + 1);
+
+			sprintf(paramname, "$%d", j + 1);
+			ri_GenerateQual(&qualbuf, qualsep,
+							paramname, pk_type,
+							riinfo->pf_eq_oprs[i],
+							attname, fk_type);
+			querysep = ",";
+			qualsep = "AND";
+			queryoids[i] = pk_type;
+			queryoids[j] = pk_type;
+		}
+		appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len);
+
+		/* Set a param for FOR PORTION OF TO/FROM */
+		queryoids[2 * riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+
+		/* Prepare and save the plan */
+		qplan = ri_PlanCheck(querybuf.data, 2 * riinfo->nkeys + 1, queryoids,
+							 &qkey, fk_rel, pk_rel);
+	}
+
+	/*
+	 * We have a plan now. Run it to update the existing references.
+	 */
+	ri_PerformCheck(riinfo, &qkey, qplan,
+					fk_rel, pk_rel,
+					oldslot, newslot,
+					riinfo->nkeys * 2 + 1, targetRange,
+					false,
+					true,		/* must detect new rows */
+					SPI_OK_UPDATE);
+}
+
 /*
  * RI_FKey_pk_upd_check_required -
  *
@@ -4868,6 +5144,46 @@ kept_enforced_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInf
 	return OidFunctionCall2(riinfo->period_intersect_proc, oldRange, newRange);
 }
 
+/*
+ * pk_scalar_keys_equal -
+ *
+ * Returns true iff an UPDATE left the non-PERIOD part of the referenced key
+ * alone.  When it did, referencing rows still agree with the referenced row
+ * wherever it kept its history, and only the history it lost needs a
+ * CASCADE/SET NULL/SET DEFAULT.
+ */
+static bool
+pk_scalar_keys_equal(const RI_ConstraintInfo *riinfo,
+					 TupleTableSlot *oldslot, TupleTableSlot *newslot)
+{
+	/* The last key column is the PERIOD, so the rest are the scalar part. */
+	for (int i = 0; i < riinfo->nkeys - 1; i++)
+	{
+		AttrNumber	attnum = riinfo->pk_attnums[i];
+		CompactAttribute *att;
+		Datum		oldvalue;
+		Datum		newvalue;
+		bool		oldisnull;
+		bool		newisnull;
+
+		oldvalue = slot_getattr(oldslot, attnum, &oldisnull);
+		newvalue = slot_getattr(newslot, attnum, &newisnull);
+		if (oldisnull || newisnull)
+			return false;
+
+		/*
+		 * Compare bytewise, just as ri_KeysEqual does for the PK table: we
+		 * must propagate a change to a value that "looks" different even if
+		 * it would compare equal.
+		 */
+		att = TupleDescCompactAttr(oldslot->tts_tupleDescriptor, attnum - 1);
+		if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
+			return false;
+	}
+
+	return true;
+}
+
 /*
  * tri_foreach_lost_range -
  *
@@ -4942,6 +5258,21 @@ tri_foreach_lost_range(const RI_ConstraintInfo *riinfo,
 	FreeExprContext(rsi.econtext, true);
 }
 
+/*
+ * tri_cascade_del_lost_range -
+ *
+ * tri_foreach_lost_range callback for ON UPDATE CASCADE: deletes the
+ * referencing rows' history for one span the referenced row lost.
+ */
+static void
+tri_cascade_del_lost_range(Datum lostRange, void *arg)
+{
+	TRI_CascadeDelContext *ctx = (TRI_CascadeDelContext *) arg;
+
+	tri_cascade_del(ctx->riinfo, ctx->fk_rel, ctx->pk_rel, ctx->oldslot,
+					lostRange);
+}
+
 /*
  * ri_restrict_lost_range -
  *
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 5d947bd88de..4596766d1f7 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -2158,7 +2158,18 @@ DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01
 --
 -- rng2rng test ON UPDATE/DELETE options
 --
+-- TOC:
+-- referenced updates CASCADE
+-- referenced deletes CASCADE
+-- referenced updates SET NULL
+-- referenced deletes SET NULL
+-- referenced updates SET DEFAULT
+-- referenced deletes SET DEFAULT
+-- referenced updates CASCADE (two scalar cols)
+-- referenced deletes CASCADE (two scalar cols)
+--
 -- test FK referenced updates CASCADE
+--
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
@@ -2168,17 +2179,127 @@ ALTER TABLE temporal_fk_rng2rng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
     ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
+-- leftovers on both sides:
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2019-01-01) | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(3 rows)
+
+-- non-FPO update:
+-- change id and not valid_at: we cascade the whole history.
+UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2019-01-01) | [7,8)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)
+ [100,101) | [2020-01-01,2021-01-01) | [7,8)
+(3 rows)
+
+-- change id and valid_at: we cascade update some, delete some.
+UPDATE temporal_rng SET id = '[6,7)', valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[7,8)' AND valid_at @> '2018-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2018-03-01) | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)
+ [100,101) | [2020-01-01,2021-01-01) | [7,8)
+(3 rows)
+
+-- change valid_at and not id: we cascade *delete* what was lost.
+UPDATE temporal_rng SET valid_at = daterange('2020-01-01', '2020-07-01')
+WHERE id = '[7,8)' AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2018-03-01) | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)
+ [100,101) | [2020-01-01,2020-07-01) | [7,8)
+(3 rows)
+
+-- losing history at both ends gives two DELETEs:
+UPDATE temporal_rng SET valid_at = daterange('2019-04-01', '2019-07-01')
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2018-03-01) | [6,7)
+ [100,101) | [2019-04-01,2019-07-01) | [7,8)
+ [100,101) | [2020-01-01,2020-07-01) | [7,8)
+(3 rows)
+
+-- growing the history changes nothing:
+UPDATE temporal_rng SET valid_at = daterange('2019-01-01', '2020-01-01')
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2018-03-01) | [6,7)
+ [100,101) | [2019-04-01,2019-07-01) | [7,8)
+ [100,101) | [2020-01-01,2020-07-01) | [7,8)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+UPDATE temporal_rng SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [200,201) | [2018-01-01,2020-01-01) | [9,10)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes CASCADE
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2019-01-01) | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(2 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_rng WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id 
+-----------+-------------------------+-----------
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(1 row)
+
 -- test FK referenced updates SET NULL
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_fk_rng2rng
+  DROP CONSTRAINT temporal_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
     ON DELETE SET NULL ON UPDATE SET NULL;
-ERROR:  constraint "temporal_fk_rng2rng_fk" for relation "temporal_fk_rng2rng" already exists
+ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
 -- test FK referenced updates SET DEFAULT
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null));
@@ -2186,11 +2307,89 @@ INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01',
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-ERROR:  constraint "temporal_fk_rng2rng_fk" for relation "temporal_fk_rng2rng" already exists
+ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_rng2rng
+  DROP CONSTRAINT temporal_fk2_rng2rng_fk,
+  ADD CONSTRAINT temporal_fk2_rng2rng_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_rng2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id1 | parent_id2 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) | [6,7)      | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)      | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id1 | parent_id2 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) | [7,8)      | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [7,8)      | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [7,8)      | [6,7)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)', '[8,9)');
+UPDATE temporal_rng2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id1 | parent_id2 
+-----------+-------------------------+------------+------------
+ [200,201) | [2018-01-01,2020-01-01) | [9,10)     | [8,9)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id1 | parent_id2 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) | [6,7)      | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(2 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+ id | valid_at | parent_id1 | parent_id2 
+----+----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)', '[8,9)');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |        valid_at         | parent_id1 | parent_id2 
+-----------+-------------------------+------------+------------
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(1 row)
+
 --
 -- test FOREIGN KEY, multirange references multirange
 --
@@ -2777,6 +2976,207 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)';
 DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+--
+-- mltrng2mltrng test ON UPDATE/DELETE options
+--
+-- TOC:
+-- referenced updates CASCADE
+-- referenced deletes CASCADE
+-- referenced updates SET NULL
+-- referenced deletes SET NULL
+-- referenced updates SET DEFAULT
+-- referenced deletes SET DEFAULT
+-- referenced updates CASCADE (two scalar cols)
+-- referenced deletes CASCADE (two scalar cols)
+--
+-- test FK referenced updates CASCADE
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
+ALTER TABLE temporal_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_mltrng
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id 
+-----------+---------------------------------------------------+-----------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [7,8)
+(2 rows)
+
+-- non-FPO update:
+-- change id and not valid_at: we cascade the whole history.
+UPDATE temporal_mltrng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id 
+-----------+---------------------------------------------------+-----------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [7,8)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [7,8)
+(2 rows)
+
+-- change id and valid_at: we cascade update some, delete some.
+UPDATE temporal_mltrng SET id = '[6,7)', valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[7,8)' AND valid_at @> '2018-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [100,101) | {[2018-01-01,2018-03-01)} | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)} | [7,8)
+(2 rows)
+
+-- change valid_at and not id: we cascade *delete* what was lost.
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2020-01-01', '2020-07-01'))
+WHERE id = '[7,8)' AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [100,101) | {[2018-01-01,2018-03-01)} | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)} | [7,8)
+(2 rows)
+
+-- losing history at both ends deletes a discontiguous multirange:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2019-04-01', '2019-07-01'))
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [100,101) | {[2018-01-01,2018-03-01)} | [6,7)
+ [100,101) | {[2019-04-01,2019-07-01)} | [7,8)
+(2 rows)
+
+-- growing the history changes nothing:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2019-01-01', '2020-01-01'))
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [100,101) | {[2018-01-01,2018-03-01)} | [6,7)
+ [100,101) | {[2019-04-01,2019-07-01)} | [7,8)
+(2 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+UPDATE temporal_mltrng SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [200,201) | {[2018-01-01,2020-01-01)} | [9,10)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes CASCADE
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id 
+-----------+---------------------------------------------------+-----------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [6,7)
+(1 row)
+
+-- non-FPO delete:
+DELETE FROM temporal_mltrng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+DELETE FROM temporal_mltrng WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id 
+-----------+---------------------------+-----------
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(1 row)
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  DROP CONSTRAINT temporal_fk2_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk2_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_mltrng2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id1 | parent_id2 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [6,7)      | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [7,8)      | [6,7)
+(2 rows)
+
+-- non-FPO update:
+UPDATE temporal_mltrng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id1 | parent_id2 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [7,8)      | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [7,8)      | [6,7)
+(2 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)', '[8,9)');
+UPDATE temporal_mltrng2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id1 | parent_id2 
+-----------+---------------------------+------------+------------
+ [200,201) | {[2018-01-01,2020-01-01)} | [9,10)     | [8,9)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+    id     |                     valid_at                      | parent_id1 | parent_id2 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [6,7)      | [6,7)
+(1 row)
+
+-- non-FPO delete:
+DELETE FROM temporal_mltrng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+ id | valid_at | parent_id1 | parent_id2 
+----+----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)', '[8,9)');
+DELETE FROM temporal_mltrng2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+    id     |         valid_at          | parent_id1 | parent_id2 
+-----------+---------------------------+------------+------------
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(1 row)
+
 --
 -- FK between partitioned tables: ranges
 --
@@ -2786,8 +3186,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
   CONSTRAINT temporal_partitioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 ) PARTITION BY LIST (id);
-CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)');
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)');
 INSERT INTO temporal_partitioned_rng (id, valid_at, name) VALUES
   ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'),
   ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -2800,8 +3200,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
   CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
 ) PARTITION BY LIST (id);
-CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)');
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)');
+CREATE TABLE tfkp2 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)');
 --
 -- partitioned FK referencing inserts
 --
@@ -2843,7 +3243,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
 -- should fail:
 UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
   WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk_1" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk_2" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 --
 -- partitioned FK referenced deletes NO ACTION
@@ -2855,21 +3255,79 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- should fail:
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
-ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk_1" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk_2" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 --
 -- partitioned FK referenced updates CASCADE
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_partitioned_fk_rng2rng
   DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng
     ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,5)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [4,5) | [2019-01-01,2020-01-01) | [7,8)
+ [4,5) | [2018-01-01,2019-01-01) | [6,7)
+ [4,5) | [2020-01-01,2021-01-01) | [6,7)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,5)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [4,5) | [2019-01-01,2020-01-01) | [7,8)
+ [4,5) | [2018-01-01,2019-01-01) | [7,8)
+ [4,5) | [2020-01-01,2021-01-01) | [7,8)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[15,16)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[15,16)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[10,11)', daterange('2018-01-01', '2021-01-01'), '[15,16)');
+UPDATE temporal_partitioned_rng SET id = '[16,17)' WHERE id = '[15,16)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,11)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [10,11) | [2018-01-01,2020-01-01) | [16,17)
+ [10,11) | [2020-01-01,2021-01-01) | [15,16)
+(2 rows)
+
 --
 -- partitioned FK referenced deletes CASCADE
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[5,6)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,6)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [5,6) | [2018-01-01,2019-01-01) | [8,9)
+ [5,6) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,6)';
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[17,18)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[17,18)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[11,12)', daterange('2018-01-01', '2021-01-01'), '[17,18)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,18)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,12)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [11,12) | [2020-01-01,2021-01-01) | [17,18)
+(1 row)
+
 --
 -- partitioned FK referenced updates SET NULL
 --
@@ -2897,6 +3355,25 @@ ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
 --
 -- partitioned FK referenced deletes SET DEFAULT
 --
+--
+-- partitioned FK attach/detach with a temporal referential action
+--
+-- Attaching a partition that already has an equivalent foreign key drops the
+-- action triggers belonging to the partition's own constraint, so this
+-- exercises the temporal action trigger functions there.
+--
+CREATE TABLE tfkp3 (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT tfkp3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT tfkp3_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_partitioned_rng
+    ON DELETE CASCADE ON UPDATE CASCADE
+);
+ALTER TABLE temporal_partitioned_fk_rng2rng ATTACH PARTITION tfkp3 FOR VALUES IN ('[25,26)');
+ALTER TABLE temporal_partitioned_fk_rng2rng DETACH PARTITION tfkp3;
+DROP TABLE tfkp3;
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
 --
@@ -2982,16 +3459,71 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenc
 --
 -- partitioned FK referenced updates CASCADE
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[4,5)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
 ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
   DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
   ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_mltrng
     ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
+UPDATE temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[4,5)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [4,5) | {[2019-01-01,2020-01-01)}                         | [7,8)
+ [4,5) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [6,7)
+(2 rows)
+
+UPDATE temporal_partitioned_mltrng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[4,5)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [4,5) | {[2019-01-01,2020-01-01)}                         | [7,8)
+ [4,5) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [7,8)
+(2 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[15,16)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[15,16)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[10,11)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[15,16)');
+UPDATE temporal_partitioned_mltrng SET id = '[16,17)' WHERE id = '[15,16)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[10,11)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [10,11) | {[2018-01-01,2020-01-01)} | [16,17)
+ [10,11) | {[2020-01-01,2021-01-01)} | [15,16)
+(2 rows)
+
 --
 -- partitioned FK referenced deletes CASCADE
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[5,6)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [5,6) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [8,9)
+(1 row)
+
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[5,6)';
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[17,18)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[17,18)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[11,12)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[17,18)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[17,18)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[11,12)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [11,12) | {[2020-01-01,2021-01-01)} | [17,18)
+(1 row)
+
 --
 -- partitioned FK referenced updates SET NULL
 --
@@ -3022,3 +3554,85 @@ ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 RESET datestyle;
+--
+-- Temporal foreign keys use EXEC_FLAG_SKIP_TRIGGERS:
+--
+-- A foreign key must finish its own work before chained triggers fire.
+-- The foreign key action uses UPDATE/DELETE FOR PORTION OF, which inserts
+-- leftovers, which causes the foreign key to be checked again, but
+-- during an invalid intermediate state.
+--
+-- Whereas user-supplied FOR PORTION OF leftovers are treated as separate
+-- statements (per the SQL:2011 standard), the internal FOR PORTION OF
+-- commands used by foreign keys should fire all triggers chained off
+-- inserting leftovers as part of the user-submitted statement. Otherwise
+-- they can encounter referential integrity violations.
+--
+-- There are tests above that fail if ExecForPortionOfLeftovers ignores
+-- EXEC_FLAG_SKIP_TRIGGERS, but this test observes the trigger timing
+-- and transition tables directly.
+--
+CREATE TABLE temporal_rng4 (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng4_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng4 (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2rng4_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2rng4_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng4 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+CREATE FUNCTION temporal_report_leftovers() RETURNS trigger LANGUAGE plpgsql AS $$
+DECLARE
+  n bigint;
+BEGIN
+  SELECT count(*) INTO n FROM newtable;
+  RAISE NOTICE 'inserted % leftover(s)', n;
+  RETURN NULL;
+END;
+$$;
+CREATE TRIGGER temporal_fk_rng2rng4_leftovers
+  AFTER INSERT ON temporal_fk_rng2rng4
+  REFERENCING NEW TABLE AS newtable
+  FOR EACH STATEMENT EXECUTE FUNCTION temporal_report_leftovers();
+INSERT INTO temporal_rng4 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng4 (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-01', '2021-01-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-01', '2021-01-01'), '[1,2)');
+NOTICE:  inserted 2 leftover(s)
+-- cascaded: both referencing rows are split, in one INSERT statement
+DELETE FROM temporal_rng4 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01';
+NOTICE:  inserted 4 leftover(s)
+SELECT * FROM temporal_fk_rng2rng4 ORDER BY id, valid_at;
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [1,2) | [01-01-2018,01-01-2019) | [1,2)
+ [1,2) | [01-01-2020,01-01-2021) | [1,2)
+ [2,3) | [01-01-2018,01-01-2019) | [1,2)
+ [2,3) | [01-01-2020,01-01-2021) | [1,2)
+(4 rows)
+
+-- written out: one INSERT statement per leftover
+DELETE FROM temporal_fk_rng2rng4 FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-07-01';
+NOTICE:  inserted 1 leftover(s)
+NOTICE:  inserted 1 leftover(s)
+NOTICE:  inserted 1 leftover(s)
+NOTICE:  inserted 1 leftover(s)
+SELECT * FROM temporal_fk_rng2rng4 ORDER BY id, valid_at;
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [1,2) | [01-01-2018,06-01-2018) | [1,2)
+ [1,2) | [07-01-2018,01-01-2019) | [1,2)
+ [1,2) | [01-01-2020,01-01-2021) | [1,2)
+ [2,3) | [01-01-2018,06-01-2018) | [1,2)
+ [2,3) | [07-01-2018,01-01-2019) | [1,2)
+ [2,3) | [01-01-2020,01-01-2021) | [1,2)
+(6 rows)
+
+DROP TABLE temporal_fk_rng2rng4;
+DROP TABLE temporal_rng4;
+DROP FUNCTION temporal_report_leftovers();
+RESET datestyle;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 880e7d99467..407d6f4844f 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -1573,8 +1573,20 @@ DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01
 --
 -- rng2rng test ON UPDATE/DELETE options
 --
+-- TOC:
+-- referenced updates CASCADE
+-- referenced deletes CASCADE
+-- referenced updates SET NULL
+-- referenced deletes SET NULL
+-- referenced updates SET DEFAULT
+-- referenced deletes SET DEFAULT
+-- referenced updates CASCADE (two scalar cols)
+-- referenced deletes CASCADE (two scalar cols)
 
+--
 -- test FK referenced updates CASCADE
+--
+
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
@@ -1584,12 +1596,62 @@ ALTER TABLE temporal_fk_rng2rng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
     ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO update:
+-- change id and not valid_at: we cascade the whole history.
+UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- change id and valid_at: we cascade update some, delete some.
+UPDATE temporal_rng SET id = '[6,7)', valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[7,8)' AND valid_at @> '2018-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- change valid_at and not id: we cascade *delete* what was lost.
+UPDATE temporal_rng SET valid_at = daterange('2020-01-01', '2020-07-01')
+WHERE id = '[7,8)' AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- losing history at both ends gives two DELETEs:
+UPDATE temporal_rng SET valid_at = daterange('2019-04-01', '2019-07-01')
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- growing the history changes nothing:
+UPDATE temporal_rng SET valid_at = daterange('2019-01-01', '2020-01-01')
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+UPDATE temporal_rng SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_rng WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
 
 -- test FK referenced updates SET NULL
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_fk_rng2rng
+  DROP CONSTRAINT temporal_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
@@ -1602,11 +1664,58 @@ INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01',
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
 
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_rng2rng
+  DROP CONSTRAINT temporal_fk2_rng2rng_fk,
+  ADD CONSTRAINT temporal_fk2_rng2rng_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_rng2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)', '[8,9)');
+UPDATE temporal_rng2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', daterange('2018-01-01', '2021-01-01'), '[8,9)', '[8,9)');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
 --
 -- test FOREIGN KEY, multirange references multirange
 --
@@ -2131,6 +2240,128 @@ WHERE id = '[5,6)';
 DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)';
 DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 
+--
+-- mltrng2mltrng test ON UPDATE/DELETE options
+--
+-- TOC:
+-- referenced updates CASCADE
+-- referenced deletes CASCADE
+-- referenced updates SET NULL
+-- referenced deletes SET NULL
+-- referenced updates SET DEFAULT
+-- referenced deletes SET DEFAULT
+-- referenced updates CASCADE (two scalar cols)
+-- referenced deletes CASCADE (two scalar cols)
+
+--
+-- test FK referenced updates CASCADE
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
+ALTER TABLE temporal_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_mltrng
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO update:
+-- change id and not valid_at: we cascade the whole history.
+UPDATE temporal_mltrng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- change id and valid_at: we cascade update some, delete some.
+UPDATE temporal_mltrng SET id = '[6,7)', valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[7,8)' AND valid_at @> '2018-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- change valid_at and not id: we cascade *delete* what was lost.
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2020-01-01', '2020-07-01'))
+WHERE id = '[7,8)' AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- losing history at both ends deletes a discontiguous multirange:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2019-04-01', '2019-07-01'))
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- growing the history changes nothing:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2019-01-01', '2020-01-01'))
+WHERE id = '[7,8)' AND valid_at @> '2019-06-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+UPDATE temporal_mltrng SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_mltrng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+DELETE FROM temporal_mltrng WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  DROP CONSTRAINT temporal_fk2_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk2_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_mltrng2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_mltrng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)', '[8,9)');
+UPDATE temporal_mltrng2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[100,101)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_mltrng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[100,101)' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VALUES ('[200,201)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)', '[8,9)');
+DELETE FROM temporal_mltrng2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, valid_at;
+
 --
 -- FK between partitioned tables: ranges
 --
@@ -2141,8 +2372,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
   CONSTRAINT temporal_partitioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 ) PARTITION BY LIST (id);
-CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)');
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)');
 INSERT INTO temporal_partitioned_rng (id, valid_at, name) VALUES
   ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'),
   ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -2156,8 +2387,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
   CONSTRAINT temporal_partitioned_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng (id, PERIOD valid_at)
 ) PARTITION BY LIST (id);
-CREATE TABLE tfkp1 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)');
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)');
+CREATE TABLE tfkp2 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)');
 
 --
 -- partitioned FK referencing inserts
@@ -2216,17 +2447,42 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange
 -- partitioned FK referenced updates CASCADE
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
 ALTER TABLE temporal_partitioned_fk_rng2rng
   DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng
     ON DELETE CASCADE ON UPDATE CASCADE;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,5)';
+UPDATE temporal_partitioned_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,5)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[15,16)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[15,16)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[10,11)', daterange('2018-01-01', '2021-01-01'), '[15,16)');
+UPDATE temporal_partitioned_rng SET id = '[16,17)' WHERE id = '[15,16)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,11)';
 
 --
 -- partitioned FK referenced deletes CASCADE
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[5,6)', daterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,6)';
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[17,18)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[17,18)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[11,12)', daterange('2018-01-01', '2021-01-01'), '[17,18)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,18)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,12)';
+
 --
 -- partitioned FK referenced updates SET NULL
 --
@@ -2258,6 +2514,27 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
 -- partitioned FK referenced deletes SET DEFAULT
 --
 
+--
+-- partitioned FK attach/detach with a temporal referential action
+--
+-- Attaching a partition that already has an equivalent foreign key drops the
+-- action triggers belonging to the partition's own constraint, so this
+-- exercises the temporal action trigger functions there.
+--
+
+CREATE TABLE tfkp3 (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT tfkp3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT tfkp3_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_partitioned_rng
+    ON DELETE CASCADE ON UPDATE CASCADE
+);
+ALTER TABLE temporal_partitioned_fk_rng2rng ATTACH PARTITION tfkp3 FOR VALUES IN ('[25,26)');
+ALTER TABLE temporal_partitioned_fk_rng2rng DETACH PARTITION tfkp3;
+DROP TABLE tfkp3;
+
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
 
@@ -2346,17 +2623,42 @@ DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemu
 -- partitioned FK referenced updates CASCADE
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[4,5)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[6,7)');
 ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
   DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
   ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_mltrng
     ON DELETE CASCADE ON UPDATE CASCADE;
+UPDATE temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[4,5)';
+UPDATE temporal_partitioned_mltrng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[4,5)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[15,16)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[15,16)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[10,11)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[15,16)');
+UPDATE temporal_partitioned_mltrng SET id = '[16,17)' WHERE id = '[15,16)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[10,11)';
 
 --
 -- partitioned FK referenced deletes CASCADE
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[8,9)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[5,6)';
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[8,9)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[17,18)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[17,18)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[11,12)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[17,18)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[17,18)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[11,12)';
+
 --
 -- partitioned FK referenced updates SET NULL
 --
@@ -2392,3 +2694,65 @@ DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
 RESET datestyle;
+
+--
+-- Temporal foreign keys use EXEC_FLAG_SKIP_TRIGGERS:
+--
+-- A foreign key must finish its own work before chained triggers fire.
+-- The foreign key action uses UPDATE/DELETE FOR PORTION OF, which inserts
+-- leftovers, which causes the foreign key to be checked again, but
+-- during an invalid intermediate state.
+--
+-- Whereas user-supplied FOR PORTION OF leftovers are treated as separate
+-- statements (per the SQL:2011 standard), the internal FOR PORTION OF
+-- commands used by foreign keys should fire all triggers chained off
+-- inserting leftovers as part of the user-submitted statement. Otherwise
+-- they can encounter referential integrity violations.
+--
+-- There are tests above that fail if ExecForPortionOfLeftovers ignores
+-- EXEC_FLAG_SKIP_TRIGGERS, but this test observes the trigger timing
+-- and transition tables directly.
+--
+
+CREATE TABLE temporal_rng4 (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng4_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng4 (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2rng4_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2rng4_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng4 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+CREATE FUNCTION temporal_report_leftovers() RETURNS trigger LANGUAGE plpgsql AS $$
+DECLARE
+  n bigint;
+BEGIN
+  SELECT count(*) INTO n FROM newtable;
+  RAISE NOTICE 'inserted % leftover(s)', n;
+  RETURN NULL;
+END;
+$$;
+CREATE TRIGGER temporal_fk_rng2rng4_leftovers
+  AFTER INSERT ON temporal_fk_rng2rng4
+  REFERENCING NEW TABLE AS newtable
+  FOR EACH STATEMENT EXECUTE FUNCTION temporal_report_leftovers();
+INSERT INTO temporal_rng4 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng4 (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-01', '2021-01-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-01', '2021-01-01'), '[1,2)');
+-- cascaded: both referencing rows are split, in one INSERT statement
+DELETE FROM temporal_rng4 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01';
+SELECT * FROM temporal_fk_rng2rng4 ORDER BY id, valid_at;
+-- written out: one INSERT statement per leftover
+DELETE FROM temporal_fk_rng2rng4 FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-07-01';
+SELECT * FROM temporal_fk_rng2rng4 ORDER BY id, valid_at;
+
+DROP TABLE temporal_fk_rng2rng4;
+DROP TABLE temporal_rng4;
+DROP FUNCTION temporal_report_leftovers();
+
+RESET datestyle;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7214cb29a81..07a847d841f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3118,6 +3118,7 @@ TParserStateAction
 TParserStateActionItem
 TQueueDestReceiver
 TRGM
+TRI_CascadeDelContext
 TRI_SetContext
 TSAnyCacheEntry
 TSConfigCacheEntry
@@ -4389,6 +4390,7 @@ tree_mutator_callback
 tree_walker_callback
 trgm
 trgm_mb_char
+tri_lost_range_callback
 trivalue
 tsKEY
 ts_parserstate
-- 
2.45.0

