Subject: Re: SQL:2011 Application Time Update & Delete

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 13 Feb 2026 12:00:52 -0800

On Wed, Feb 11, 2026 at 1:25 PM Paul A Jungwirth
<[email-masked]> wrote:
>
> On Thu, Jan 22, 2026 at 7:21 AM Peter Eisentraut <[email-masked]> wrote:
> >
> > I have committed the pg_range patch.
>
> Thanks! Here are v65 patches for UPDATE/DELETE FOR PORTION OF. I kept
> the get_range_constructor2 helper function as a separate patch, but it
> probably doesn't really need to be a separate commit. Maybe it could
> even be inlined into its caller.

Here is another round to fix a few rebase conflicts.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From ce5d7740282e87e9f3f93bdfe6a316098e70ef77 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 2 Dec 2025 21:30:13 -0800
Subject: [PATCH v66 1/7] Add range_get_constructor2 to lsyscache

Look up the two-arg constructor for a given rangetype. We need this for
UPDATE/DELETE FOR PORTION OF, so that we can build a range from the FROM/TO
bounds.

Author: Paul A. Jungwirth <[email-masked]>
---
 src/backend/utils/cache/lsyscache.c | 25 +++++++++++++++++++++++++
 src/include/utils/lsyscache.h       |  1 +
 2 files changed, 26 insertions(+)

diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index b924a2d900b..5fd074afe1e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3598,6 +3598,31 @@ get_range_collation(Oid rangeOid)
 		return InvalidOid;
 }
 
+/*
+ * get_range_constructor2
+ *		Gets the 2-arg constructor for the given rangetype.
+ *
+ *	Raises an error if not found.
+ */
+RegProcedure
+get_range_constructor2(Oid rangeOid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(RANGETYPE, ObjectIdGetDatum(rangeOid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tp);
+		RegProcedure	result;
+
+		result = rngtup->rngconstruct2;
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		elog(ERROR, "cache lookup failed for range type %u", rangeOid);
+}
+
 /*
  * get_range_multirange
  *		Returns the multirange type of a given range type
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 5655aca4c14..5b9d1460e66 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -200,6 +200,7 @@ extern char *get_namespace_name(Oid nspid);
 extern char *get_namespace_name_or_temp(Oid nspid);
 extern Oid	get_range_subtype(Oid rangeOid);
 extern Oid	get_range_collation(Oid rangeOid);
+extern Oid	get_range_constructor2(Oid rangeOid);
 extern Oid	get_range_multirange(Oid rangeOid);
 extern Oid	get_multirange_range(Oid multirangeOid);
 extern Oid	get_index_column_opclass(Oid index_oid, int attno);
-- 
2.47.3

From 94d36b01c6fa32965e7462a51b96e7ef666744dc Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v66 4/7] Add tg_temporal to TriggerData

This needs to be passed to our RI triggers to implement temporal
CASCADE/SET NULL/SET DEFAULT when the user command is an UPDATE/DELETE
FOR PORTION OF. The triggers will use the FOR PORTION OF bounds to avoid
over-applying the change to referencing records.

Probably it is useful for user-defined triggers as well, for example
auditing or trigger-based replication.

Author: Paul A. Jungwirth <[email-masked]>
---
 doc/src/sgml/trigger.sgml      | 56 +++++++++++++++++++++++++++-------
 src/backend/commands/trigger.c | 51 +++++++++++++++++++++++++++++++
 src/include/commands/trigger.h |  1 +
 3 files changed, 97 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 2b68c3882ec..cfc084b34c6 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -563,17 +563,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>
 
@@ -841,6 +842,39 @@ 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>.
+        Contains a pointer to a structure of type
+        <structname>ForPortionOfState</structname>, defined in
+        <filename>nodes/execnodes.h</filename>:
+
+<programlisting>
+typedef struct ForPortionOfState
+{
+    NodeTag     type;
+
+    char       *fp_rangeName;   /* the column named in FOR PORTION OF */
+    Oid         fp_rangeType;   /* the type of the FOR PORTION OF expression */
+    int         fp_rangeAttno;  /* the attno of the range column */
+    Datum       fp_targetRange; /* the range/multirange from FOR PORTION OF */
+    TypeCacheEntry *fp_leftoverstypcache;   /* type cache entry of the range */
+} ForPortionOfState;
+</programlisting>
+
+       where <structfield>fp_rangeName</structfield> is the range
+       column named in the <literal>FOR PORTION OF</literal> clause,
+       <structfield>fp_rangeType</structfield> is its range type,
+       <structfield>fp_rangeAttno</structfield> is its attribute number,
+       and <structfield>fp_targetRange</structfield> is a rangetype value created
+       by evaluating the <literal>FOR PORTION OF</literal> bounds.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 8df915f63fb..fef9726bab4 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -47,12 +47,14 @@
 #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"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/plancache.h"
+#include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -2649,6 +2651,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];
@@ -2757,6 +2760,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;
@@ -2858,6 +2862,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);
 
@@ -2921,6 +2926,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];
@@ -3064,6 +3070,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++)
@@ -3226,6 +3233,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);
 
@@ -3697,6 +3705,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 *for_portion_of;	/* the FOR PORTION OF clause */
 	struct AfterTriggersTableData *ats_table;	/* transition table access */
 	Bitmapset  *ats_modifiedcols;	/* modified columns */
 } AfterTriggerSharedData;
@@ -3960,6 +3969,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);
 static SetConstraintState SetConstraintStateCopy(SetConstraintState origstate);
 static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
 													Oid tgoid, bool tgisdeferred);
+static ForPortionOfState *CopyForPortionOfState(ForPortionOfState *src);
 static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);

@@ -4167,6 +4177,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
 			newshared->ats_event == evtshared->ats_event &&
 			newshared->ats_firing_id == 0 &&
 			newshared->ats_table == evtshared->ats_table &&
+			newshared->for_portion_of == evtshared->for_portion_of &&
 			newshared->ats_relid == evtshared->ats_relid &&
 			newshared->ats_rolid == evtshared->ats_rolid &&
 			bms_equal(newshared->ats_modifiedcols,
@@ -4537,6 +4548,9 @@ AfterTriggerExecute(EState *estate,
 	LocTriggerData.tg_relation = rel;
 	if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
 		LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
+	if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype) ||
+		TRIGGER_FOR_DELETE(LocTriggerData.tg_trigger->tgtype))
+		LocTriggerData.tg_temporal = evtshared->for_portion_of;
 
 	MemoryContextReset(per_tuple_context);
 
@@ -6123,6 +6137,42 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
+ * Copys a ForPortionOfState into the current memory context.
+ */
+static ForPortionOfState *
+CopyForPortionOfState(ForPortionOfState *src)
+{
+	ForPortionOfState *dst = NULL;
+
+	if (src)
+	{
+		MemoryContext oldctx;
+		RangeType  *r;
+		TypeCacheEntry *typcache;
+
+		/*
+		 * Need to lift the FOR PORTION OF details into a higher memory
+		 * context because cascading foreign key update/deletes can cause
+		 * triggers to fire triggers, and the AfterTriggerEvents will outlive
+		 * the FPO details of the original query.
+		 */
+		oldctx = MemoryContextSwitchTo(TopTransactionContext);
+		dst = makeNode(ForPortionOfState);
+		dst->fp_rangeName = pstrdup(src->fp_rangeName);
+		dst->fp_rangeType = src->fp_rangeType;
+		dst->fp_rangeAttno = src->fp_rangeAttno;
+
+		r = DatumGetRangeTypeP(src->fp_targetRange);
+		typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO);
+		dst->fp_targetRange = datumCopy(src->fp_targetRange, typcache->typbyval, typcache->typlen);
+		MemoryContextSwitchTo(oldctx);
+	}
+	return dst;
+}
+
 /* ----------
  * AfterTriggerSaveEvent()
  *
@@ -6556,6 +6606,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 		else
 			new_shared.ats_table = NULL;
 		new_shared.ats_modifiedcols = modifiedCols;
+		new_shared.for_portion_of = CopyForPortionOfState(relinfo->ri_forPortionOf);
 
 		afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
 							 &new_event, &new_shared);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 556c86bf5e1..1e4f7903119 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;
 
 /*
-- 
2.47.3

From 0bcb268052fd631d507192543bbb98df903d6c89 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 31 Oct 2025 19:59:52 -0700
Subject: [PATCH v66 3/7] Add isolation tests for UPDATE/DELETE FOR PORTION OF

Concurrent updates/deletes in READ COMMITTED mode don't give you what you want:
the second update/delete fails to leftovers from the first, so you essentially
have lost updates/deletes. But we are following the rules, and other RDBMSes
give you screwy results in READ COMMITTED too (albeit different).

One approach is to lock the history you want with SELECT FOR UPDATE before
issuing the actual UPDATE/DELETE. That way you see the leftovers of anyone else
who also touched that history. The isolation tests here use that approach and
show that it's viable.

Author: Paul A. Jungwirth <[email-masked]>
---
 doc/src/sgml/dml.sgml                         |   16 +
 src/backend/executor/nodeModifyTable.c        |    4 +
 .../isolation/expected/for-portion-of.out     | 5803 +++++++++++++++++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/for-portion-of.spec  |  750 +++
 5 files changed, 6574 insertions(+)
 create mode 100644 src/test/isolation/expected/for-portion-of.out
 create mode 100644 src/test/isolation/specs/for-portion-of.spec

diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
index 08c0e759719..ac69be756d5 100644
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -393,6 +393,22 @@ WHERE product_no = 5;
    column references are not.
   </para>
 
+  <para>
+   In <literal>READ COMMITTED</literal> mode, temporal updates and deletes can
+   yield unexpected results when they concurrently touch the same row. It is
+   possible to lose all or part of the second update or delete. That's because
+   after the first update changes the start/end times of the original
+   record, it may no longer fit within the second query's <literal>FOR PORTION
+   OF</literal> bounds, so it becomes disqualified from the query. On the other
+   hand the just-inserted temporal leftovers may be overlooked by the second query,
+   which has already scanned the table to find rows to modify. To solve these
+   problems, precede every temporal update/delete with a <literal>SELECT FOR
+   UPDATE</literal> matching the same criteria (including the targeted portion of
+   application time). That way the actual update/delete doesn't begin until the
+   lock is held, and all concurrent leftovers will be visible. In other
+   transaction isolation levels, this lock is not required.
+  </para>
+
   <para>
    When temporal leftovers are inserted, all <literal>INSERT</literal>
    triggers are fired, but permission checks for inserting rows are
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 38a0901b658..2560abf7ef9 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1464,6 +1464,10 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	 * We have already locked the tuple in ExecUpdate/ExecDelete, and it has
 	 * passed EvalPlanQual. This ensures that concurrent updates in READ
 	 * COMMITTED can't insert conflicting temporal leftovers.
+	 *
+	 * It does *not* protect against concurrent update/deletes overlooking
+	 * each others' leftovers though. See our isolation tests for details
+	 * about that and a viable workaround.
 	 */
 	if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot))
 		elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
diff --git a/src/test/isolation/expected/for-portion-of.out b/src/test/isolation/expected/for-portion-of.out
new file mode 100644
index 00000000000..89f646dd899
--- /dev/null
+++ b/src/test/isolation/expected/for-portion-of.out
@@ -0,0 +1,5803 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+(2 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2upd2027 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2upd202503 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2upd20252026 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)|10.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-03-01,2025-04-01)|10.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-06-01,2026-06-01)|10.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2del2027 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2del202503 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2del20252026 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1upd2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+(2 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2upd2027 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2upd202503 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2upd20252026 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-03-01,2025-04-01)|10.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2025-06-01,2026-06-01)|10.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2del2027 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2del202503 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2del20252026 s2c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock2027: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock202503: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1del2025 s1c s1q
+step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s2lock20252026: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1lock2025: 
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+ <waiting ...>
+step s2c: COMMIT;
+step s1lock2025: <... completed>
+id|valid_at|price
+--+--------+-----
+(0 rows)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2upd2027 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s2upd202503 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s2upd20252026 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2upd2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2upd202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2upd20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)|10.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s2upd2027 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2upd202503 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2upd20252026 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd2027 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd202503 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd20252026 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)|10.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd2027 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd202503 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd20252026 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2del2027 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s2del202503 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s2del20252026 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2del2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2del202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1upd2025 s1c s2del20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2del2027 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del202503 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del20252026 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del2027 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del202503 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del20252026 s2c s1upd2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del2027 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del202503 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del20252026 s1upd2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2upd2027 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s2upd202503 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2upd20252026 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2upd2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2upd202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2upd20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2upd2027 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2upd202503 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2upd20252026 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd2027 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd202503 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd20252026 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd2027 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd202503 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s2upd20252026 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2del2027 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s2del202503 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del20252026 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2del2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2del202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1del2025 s1c s2del20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del2027 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del202503 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s2del20252026 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del2027 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del202503 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del20252026 s2c s1del2025 s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2del2027 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2del202503 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s1del2025 s1c s2del20252026 s2c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del2027 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del202503 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1rr s2rr s1q s2del20252026 s1del2025 s2c s1c s1q
+step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2upd2027 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s2upd202503 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s2upd20252026 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2upd2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2upd202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2upd20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)|10.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s2upd2027 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2upd202503 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2upd20252026 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd2027 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd202503 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd20252026 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2upd2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2upd202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2upd20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2025-06-01,2026-01-01)|10.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(5 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd2027 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd202503 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd20252026 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2del2027 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s2del202503 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s2del20252026 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2del2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2del202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1upd2025 s1c s2del20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2del2027 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del202503 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del20252026 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del2027 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del202503 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del20252026 s2c s1upd2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2del2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2del202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-03-01)| 8.00
+[1,2)|[2025-04-01,2026-01-01)| 8.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1q s1upd2025 s1c s2del20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2025-01-01,2025-06-01)| 8.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del2027 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del202503 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del20252026 s1upd2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1upd2025: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1upd2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2upd2027 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s2upd202503 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2upd20252026 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2upd2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2upd202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2upd20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2upd2027 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2upd202503 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2upd20252026 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd2027 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd202503 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd20252026 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2upd2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(4 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2upd202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2upd20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd2027 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd2027: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2027-01-01,2028-01-01)|10.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd202503 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd202503: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-03-01,2025-04-01)|10.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s2upd20252026 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2upd20252026: 
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent update
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2025-06-01,2026-06-01)|10.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2del2027 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s2del202503 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del20252026 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2del2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2del202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1del2025 s1c s2del20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del2027 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del202503 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s2del20252026 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del2027 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del202503 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del20252026 s2c s1del2025 s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2del2027 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(3 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2del202503 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s1del2025 s1c s2del20252026 s2c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+
+step s1c: COMMIT;
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s2c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-01-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del2027 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del2027: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2027-01-01)| 5.00
+[1,2)|[2028-01-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del202503 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del202503: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-03-01)| 5.00
+[1,2)|[2025-04-01,2030-01-01)| 5.00
+(2 rows)
+
+
+starting permutation: s1ser s2ser s1q s2del20252026 s1del2025 s2c s1c s1q
+step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2030-01-01)| 5.00
+(1 row)
+
+step s2del20252026: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+
+step s1del2025: 
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+ <waiting ...>
+step s2c: COMMIT;
+step s1del2025: <... completed>
+ERROR:  could not serialize access due to concurrent delete
+step s1c: COMMIT;
+step s1q: SELECT * FROM products ORDER BY id, valid_at;
+id   |valid_at               |price
+-----+-----------------------+-----
+[1,2)|[2020-01-01,2025-06-01)| 5.00
+[1,2)|[2026-06-01,2030-01-01)| 5.00
+(2 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..16312a3be5f 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -124,3 +124,4 @@ test: serializable-parallel-2
 test: serializable-parallel-3
 test: matview-write-skew
 test: lock-nowait
+test: for-portion-of
diff --git a/src/test/isolation/specs/for-portion-of.spec b/src/test/isolation/specs/for-portion-of.spec
new file mode 100644
index 00000000000..942efd439ba
--- /dev/null
+++ b/src/test/isolation/specs/for-portion-of.spec
@@ -0,0 +1,750 @@
+# UPDATE/DELETE FOR PORTION OF test
+#
+# Test inserting temporal leftovers from a FOR PORTION OF update/delete.
+#
+# In READ COMMITTED mode, concurrent updates/deletes to the same records cause
+# weird results. Portions of history that should have been updated/deleted don't
+# get changed. That's because the leftovers from one operation are added too
+# late to be seen by the other. EvalPlanQual will reload the changed-in-common
+# row, but it won't re-scan to find new leftovers.
+#
+# MariaDB similarly gives undesirable results in READ COMMITTED mode (although
+# not the same results). DB2 doesn't have READ COMMITTED, but it gives correct
+# results at all levels, in particular READ STABILITY (which seems closest).
+#
+# A workaround is to lock the part of history you want before changing it (using
+# SELECT FOR UPDATE). That way the search for rows is late enough to see
+# leftovers from the other session(s). This shouldn't impose any new deadlock
+# risks, since the locks are the same as before. Adding a third/fourth/etc.
+# connection also doesn't change the semantics. The READ COMMITTED tests here
+# use that approach to prove that it's viable and isn't vitiated by any bugs.
+# Incidentally, this approach also works in MariaDB.
+#
+# We run the same tests under REPEATABLE READ and SERIALIZABLE.
+# In general they do what you'd want with no explicit locking required, but some
+# orderings raise a concurrent update/delete failure (as expected). If there is
+# a prior read by s1, concurrent update/delete failures are more common.
+#
+# We test updates where s2 updates history that is:
+#
+# - non-overlapping with s1,
+# - contained entirely in s1,
+# - partly contained in s1.
+#
+# We don't need to test where s2 entirely contains s1 because of symmetry:
+# we test both when s1 precedes s2 and when s2 precedes s1,  so that scenario is
+# covered.
+#
+# We test various orderings of the update/delete/commit from s1 and s2.
+# Note that `s1lock s2lock s1change` is boring because it's the same as
+# `s1lock s1change s2lock`. In other words it doesn't matter if something
+# interposes between the lock and its change (as long as everyone is following
+# the same policy).
+
+setup
+{
+  CREATE TABLE products (
+	id int4range NOT NULL,
+	valid_at daterange NOT NULL,
+	price decimal NOT NULL,
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS));
+  INSERT INTO products VALUES
+	('[1,2)', '[2020-01-01,2030-01-01)', 5.00);
+}
+
+teardown { DROP TABLE products; }
+
+session s1
+setup		{ SET datestyle TO ISO, YMD; }
+step s1rc	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s1rr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s1ser	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s1lock2025 {
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+}
+step s1upd2025	{
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  SET price = 8.00
+  WHERE id = '[1,2)';
+}
+step s1del2025 {
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01'
+  WHERE id = '[1,2)';
+}
+step s1q	{ SELECT * FROM products ORDER BY id, valid_at; }
+step s1c	{ COMMIT; }
+
+session s2
+setup		{ SET datestyle TO ISO, YMD; }
+step s2rc	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s2rr	{ BEGIN ISOLATION LEVEL REPEATABLE READ; }
+step s2ser	{ BEGIN ISOLATION LEVEL SERIALIZABLE; }
+step s2lock202503 {
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)'
+  ORDER BY valid_at FOR UPDATE;
+}
+step s2lock20252026 {
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)'
+  ORDER BY valid_at FOR UPDATE;
+}
+step s2lock2027 {
+  SELECT * FROM products
+  WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)'
+  ORDER BY valid_at FOR UPDATE;
+}
+step s2upd202503 {
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+}
+step s2upd20252026 {
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+}
+step s2upd2027 {
+  UPDATE products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  SET price = 10.00
+  WHERE id = '[1,2)';
+}
+step s2del202503 {
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01'
+  WHERE id = '[1,2)';
+}
+step s2del20252026 {
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01'
+  WHERE id = '[1,2)';
+}
+step s2del2027 {
+  DELETE FROM products
+  FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01'
+  WHERE id = '[1,2)';
+}
+step s2c	{ COMMIT; }
+
+# ########################################
+# READ COMMITTED tests, UPDATE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2upd2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2upd202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2upd20252026 s2c s1q
+
+# s1 updates the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1upd2025 s1c s1q
+
+# s1 overwrites the row from s2 and sees its leftovers
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1upd2025 s1c s1q
+
+# s1 overwrites the row from s2 and sees its leftovers
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1upd2025 s1c s1q
+
+# ########################################
+# READ COMMITTED tests, UPDATE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2del2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2del202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2del20252026 s2c s1q
+
+# s1 updates the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1upd2025 s1c s1q
+
+# s1 sees the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1upd2025 s1c s1q
+
+# s1 sees the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1upd2025 s1c s1q
+
+# ########################################
+# READ COMMITTED tests, DELETE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2upd2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2upd202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2upd20252026 s2c s1q
+
+# s1 deletes the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1del2025 s1c s1q
+
+# s1 deletes the new row from s2 and its leftovers
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1del2025 s1c s1q
+
+# s1 deletes the new row from s2 and its leftovers
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1del2025 s1c s1q
+
+# ########################################
+# READ COMMITTED tests, DELETE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1del2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1del2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2del2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2del202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2del20252026 s2c s1q
+
+# s1 deletes the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1del2025 s1c s1q
+
+# s1 deletes the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1del2025 s1c s1q
+
+# s1 deletes the leftovers from s2
+# Locking is required or s1 won't see the leftovers.
+permutation s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1del2025 s1c s1q
+
+# ########################################
+# REPEATABLE READ tests, UPDATE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rr s2rr s2upd2027 s2c s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2upd202503 s2c s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2upd20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1upd2025 s1c s2upd2027 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rr s2rr s1upd2025 s1c s2upd202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rr s2rr s1upd2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd20252026 s1upd2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd2027 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd202503 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1q s1upd2025 s1c s2upd2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1rr s2rr s1q s1upd2025 s1c s2upd202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rr s2rr s1q s1upd2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd20252026 s1upd2025 s2c s1c s1q
+
+# ########################################
+# REPEATABLE READ tests, UPDATE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rr s2rr s2del2027 s2c s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s2del202503 s2c s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s2del20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1upd2025 s1c s2del2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1rr s2rr s1upd2025 s1c s2del202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rr s2rr s1upd2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del20252026 s1upd2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del2027 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del202503 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1q s1upd2025 s1c s2del2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1rr s2rr s1q s1upd2025 s1c s2del202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1rr s2rr s1q s1upd2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del20252026 s1upd2025 s2c s1c s1q
+
+# ########################################
+# REPEATABLE READ tests, DELETE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rr s2rr s2upd2027 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2upd202503 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2upd20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1del2025 s1c s2upd2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rr s2rr s1del2025 s1c s2upd202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s1del2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s2upd20252026 s1del2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd2027 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd202503 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1q s1del2025 s1c s2upd2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rr s2rr s1q s1del2025 s1c s2upd202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s1q s1del2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1rr s2rr s1q s2upd20252026 s1del2025 s2c s1c s1q
+
+# ########################################
+# REPEATABLE READ tests, DELETE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1rr s2rr s2del2027 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2del202503 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1rr s2rr s2del20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1del2025 s1c s2del2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rr s2rr s1del2025 s1c s2del202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s1del2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s2del20252026 s1del2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del2027 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del202503 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1rr s2rr s1q s1del2025 s1c s2del2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1rr s2rr s1q s1del2025 s1c s2del202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1rr s2rr s1q s1del2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1rr s2rr s1q s2del20252026 s1del2025 s2c s1c s1q
+
+# ########################################
+# SERIALIZABLE tests, UPDATE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1ser s2ser s2upd2027 s2c s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1ser s2ser s2upd202503 s2c s1upd2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1ser s2ser s2upd20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1upd2025 s1c s2upd2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1ser s2ser s1upd2025 s1c s2upd202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1ser s2ser s1upd2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd20252026 s1upd2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd2027 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd202503 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1q s1upd2025 s1c s2upd2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1ser s2ser s1q s1upd2025 s1c s2upd202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1ser s2ser s1q s1upd2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd20252026 s1upd2025 s2c s1c s1q
+
+# ########################################
+# SERIALIZABLE tests, UPDATE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1ser s2ser s2del2027 s2c s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s2del202503 s2c s1upd2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s2del20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1upd2025 s1c s2del2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1ser s2ser s1upd2025 s1c s2del202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1ser s2ser s1upd2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del20252026 s1upd2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del2027 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del202503 s2c s1upd2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del20252026 s2c s1upd2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1q s1upd2025 s1c s2del2027 s2c s1q
+
+# s2 loads the updated row
+permutation s1ser s2ser s1q s1upd2025 s1c s2del202503 s2c s1q
+
+# s2 loads the updated row and sees its leftovers
+permutation s1ser s2ser s1q s1upd2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del2027 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del202503 s1upd2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del20252026 s1upd2025 s2c s1c s1q
+
+# ########################################
+# SERIALIZABLE tests, DELETE+UPDATE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1ser s2ser s2upd2027 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1ser s2ser s2upd202503 s2c s1del2025 s1c s1q
+
+# s1 reloads the updated row and sees its leftovers
+permutation s1ser s2ser s2upd20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1del2025 s1c s2upd2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1ser s2ser s1del2025 s1c s2upd202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s1del2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s2upd20252026 s1del2025 s2c s1c s1q
+
+## with prior read by s1:
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd2027 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd202503 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1q s1del2025 s1c s2upd2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1ser s2ser s1q s1del2025 s1c s2upd202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s1q s1del2025 s1c s2upd20252026 s2c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent update
+permutation s1ser s2ser s1q s2upd20252026 s1del2025 s2c s1c s1q
+
+# ########################################
+# SERIALIZABLE tests, DELETE+DELETE:
+# ########################################
+
+# s1 sees the leftovers
+permutation s1ser s2ser s2del2027 s2c s1del2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s2del202503 s2c s1del2025 s1c s1q
+
+# s1 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s2del20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1del2025 s1c s2del2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1ser s2ser s1del2025 s1c s2del202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s1del2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s2del20252026 s1del2025 s2c s1c s1q
+
+# with prior read by s1:
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del2027 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del202503 s2c s1del2025 s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del20252026 s2c s1del2025 s1c s1q
+
+# s2 sees the leftovers
+permutation s1ser s2ser s1q s1del2025 s1c s2del2027 s2c s1q
+
+# s2 ignores the deleted row
+permutation s1ser s2ser s1q s1del2025 s1c s2del202503 s2c s1q
+
+# s2 ignores the deleted row and sees its leftovers
+permutation s1ser s2ser s1q s1del2025 s1c s2del20252026 s2c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del2027 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del202503 s1del2025 s2c s1c s1q
+
+# s1 fails from concurrent delete
+permutation s1ser s2ser s1q s2del20252026 s1del2025 s2c s1c s1q
-- 
2.47.3

From 9df6a741ae723fcc1665ee9373300ca29435217f Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v66 2/7] Add UPDATE/DELETE FOR PORTION OF

This is an extension of the UPDATE and DELETE commands to do a "temporal
update/delete" based on a range or multirange column. The user can say UPDATE t
FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET ... (or likewise
with DELETE) where valid_at is a range or multirange column.

The command is automatically limited to rows overlapping the targeted
portion, and only history within those bounds is changed. If a row
represents history partly inside and partly outside the bounds, then
the command truncates the row's application time to fit within the targeted
portion, then it inserts one or more "temporal leftovers": new rows
containing all the original values, except with the application-time
column changed to only represent the untouched part of history.

To compute the temporal leftovers that are required, we use the without_portion
set-returning functions defined in 5eed8ce50c.

- Added bison support for FOR PORTION OF syntax. The bounds must be
  constant, so we forbid column references, subqueries, etc. We do
  accept functions like NOW().
- Added logic to executor to insert new rows for the "temporal leftover"
  part of a record touched by a FOR PORTION OF query.
- Documented FOR PORTION OF.
- Added tests.

Author: Paul A. Jungwirth <[email-masked]>
---
 .../postgres_fdw/expected/postgres_fdw.out    |   45 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   34 +
 contrib/test_decoding/expected/ddl.out        |   52 +
 contrib/test_decoding/sql/ddl.sql             |   30 +
 doc/src/sgml/dml.sgml                         |  139 ++
 doc/src/sgml/glossary.sgml                    |   15 +
 doc/src/sgml/images/Makefile                  |    4 +-
 doc/src/sgml/images/temporal-delete.svg       |   41 +
 doc/src/sgml/images/temporal-delete.txt       |   10 +
 doc/src/sgml/images/temporal-update.svg       |   45 +
 doc/src/sgml/images/temporal-update.txt       |   10 +
 doc/src/sgml/ref/create_publication.sgml      |    6 +
 doc/src/sgml/ref/delete.sgml                  |  116 +-
 doc/src/sgml/ref/update.sgml                  |  117 +-
 doc/src/sgml/trigger.sgml                     |    9 +
 src/backend/executor/execMain.c               |    1 +
 src/backend/executor/nodeModifyTable.c        |  352 ++-
 src/backend/nodes/nodeFuncs.c                 |   33 +
 src/backend/optimizer/plan/createplan.c       |    6 +-
 src/backend/optimizer/plan/planner.c          |    1 +
 src/backend/optimizer/util/pathnode.c         |    3 +-
 src/backend/parser/analyze.c                  |  359 ++-
 src/backend/parser/gram.y                     |   99 +-
 src/backend/parser/parse_agg.c                |   10 +
 src/backend/parser/parse_collate.c            |    1 +
 src/backend/parser/parse_expr.c               |    8 +
 src/backend/parser/parse_func.c               |    3 +
 src/backend/parser/parse_merge.c              |    2 +-
 src/backend/rewrite/rewriteHandler.c          |   75 +-
 src/backend/utils/adt/ruleutils.c             |   41 +
 src/include/nodes/execnodes.h                 |   22 +
 src/include/nodes/parsenodes.h                |   20 +
 src/include/nodes/pathnodes.h                 |    1 +
 src/include/nodes/plannodes.h                 |    2 +
 src/include/nodes/primnodes.h                 |   33 +
 src/include/optimizer/pathnode.h              |    2 +-
 src/include/parser/analyze.h                  |    3 +-
 src/include/parser/kwlist.h                   |    1 +
 src/include/parser/parse_node.h               |    1 +
 src/test/regress/expected/for_portion_of.out  | 2067 +++++++++++++++++
 src/test/regress/expected/privileges.out      |   28 +
 src/test/regress/expected/updatable_views.out |   32 +
 .../regress/expected/without_overlaps.out     |  245 +-
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/for_portion_of.sql       | 1356 +++++++++++
 src/test/regress/sql/privileges.sql           |   27 +
 src/test/regress/sql/updatable_views.sql      |   14 +
 src/test/regress/sql/without_overlaps.sql     |  120 +-
 src/test/subscription/t/034_temporal.pl       |   85 +-
 src/tools/pgindent/typedefs.list              |    3 +
 50 files changed, 5641 insertions(+), 90 deletions(-)
 create mode 100644 doc/src/sgml/images/temporal-delete.svg
 create mode 100644 doc/src/sgml/images/temporal-delete.txt
 create mode 100644 doc/src/sgml/images/temporal-update.svg
 create mode 100644 doc/src/sgml/images/temporal-update.txt
 create mode 100644 src/test/regress/expected/for_portion_of.out
 create mode 100644 src/test/regress/sql/for_portion_of.sql

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 2ccb72c539a..c8a139f08c1 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -50,11 +50,19 @@ CREATE TABLE "S 1"."T 4" (
 	c3 text,
 	CONSTRAINT t4_pkey PRIMARY KEY (c1)
 );
+CREATE TABLE "S 1"."T 5" (
+	c1 int4range NOT NULL,
+	c2 int NOT NULL,
+	c3 text,
+	c4 daterange NOT NULL,
+	CONSTRAINT t5_pkey PRIMARY KEY (c1, c4 WITHOUT OVERLAPS)
+);
 -- Disable autovacuum for these tables to avoid unexpected effects of that
 ALTER TABLE "S 1"."T 1" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 2" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 3" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 4" SET (autovacuum_enabled = 'false');
+ALTER TABLE "S 1"."T 5" SET (autovacuum_enabled = 'false');
 INSERT INTO "S 1"."T 1"
 	SELECT id,
 	       id % 10,
@@ -81,10 +89,17 @@ INSERT INTO "S 1"."T 4"
 	       'AAA' || to_char(id, 'FM000')
 	FROM generate_series(1, 100) id;
 DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0;	-- delete for outer join tests
+INSERT INTO "S 1"."T 5"
+	SELECT int4range(id, id + 1),
+	       id + 1,
+	       'AAA' || to_char(id, 'FM000'),
+        '[2000-01-01,2020-01-01)'
+	FROM generate_series(1, 100) id;
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";
 -- ===================================================================
 -- create foreign tables
 -- ===================================================================
@@ -132,6 +147,12 @@ CREATE FOREIGN TABLE ft7 (
 	c2 int NOT NULL,
 	c3 text
 ) SERVER loopback3 OPTIONS (schema_name 'S 1', table_name 'T 4');
+CREATE FOREIGN TABLE ft8 (
+	c1 int4range NOT NULL,
+	c2 int NOT NULL,
+	c3 text,
+	c4 daterange NOT NULL
+) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5');
 -- ===================================================================
 -- tests for validator
 -- ===================================================================
@@ -214,7 +235,8 @@ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
  public | ft5   | loopback  | (schema_name 'S 1', table_name 'T 4') | 
  public | ft6   | loopback2 | (schema_name 'S 1', table_name 'T 4') | 
  public | ft7   | loopback3 | (schema_name 'S 1', table_name 'T 4') | 
-(6 rows)
+ public | ft8   | loopback  | (schema_name 'S 1', table_name 'T 5') | 
+(7 rows)
 
 -- Test that alteration of server options causes reconnection
 -- Remote's errors might be non-English, so hide them to ensure stable results
@@ -6303,6 +6325,27 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass;
  ft2
 (1 row)
 
+-- Test UPDATE FOR PORTION OF
+UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01'
+SET c2 = c2 + 1
+WHERE c1 = '[1,2)';
+ERROR:  foreign tables don't support FOR PORTION OF
+SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4;
+  c1   | c2 |   c3   |           c4            
+-------+----+--------+-------------------------
+ [1,2) |  2 | AAA001 | [01-01-2000,01-01-2020)
+(1 row)
+
+-- Test DELETE FOR PORTION OF
+DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01'
+WHERE c1 = '[2,3)';
+ERROR:  foreign tables don't support FOR PORTION OF
+SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4;
+  c1   | c2 |   c3   |           c4            
+-------+----+--------+-------------------------
+ [2,3) |  3 | AAA002 | [01-01-2000,01-01-2020)
+(1 row)
+
 -- Test UPDATE/DELETE with RETURNING on a three-table join
 INSERT INTO ft2 (c1,c2,c3)
   SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 72d2d9c311b..410b9ac1404 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -54,12 +54,20 @@ CREATE TABLE "S 1"."T 4" (
 	c3 text,
 	CONSTRAINT t4_pkey PRIMARY KEY (c1)
 );
+CREATE TABLE "S 1"."T 5" (
+	c1 int4range NOT NULL,
+	c2 int NOT NULL,
+	c3 text,
+	c4 daterange NOT NULL,
+	CONSTRAINT t5_pkey PRIMARY KEY (c1, c4 WITHOUT OVERLAPS)
+);
 
 -- Disable autovacuum for these tables to avoid unexpected effects of that
 ALTER TABLE "S 1"."T 1" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 2" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 3" SET (autovacuum_enabled = 'false');
 ALTER TABLE "S 1"."T 4" SET (autovacuum_enabled = 'false');
+ALTER TABLE "S 1"."T 5" SET (autovacuum_enabled = 'false');
 
 INSERT INTO "S 1"."T 1"
 	SELECT id,
@@ -87,11 +95,18 @@ INSERT INTO "S 1"."T 4"
 	       'AAA' || to_char(id, 'FM000')
 	FROM generate_series(1, 100) id;
 DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0;	-- delete for outer join tests
+INSERT INTO "S 1"."T 5"
+	SELECT int4range(id, id + 1),
+	       id + 1,
+	       'AAA' || to_char(id, 'FM000'),
+        '[2000-01-01,2020-01-01)'
+	FROM generate_series(1, 100) id;
 
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";
 
 -- ===================================================================
 -- create foreign tables
@@ -146,6 +161,14 @@ CREATE FOREIGN TABLE ft7 (
 	c3 text
 ) SERVER loopback3 OPTIONS (schema_name 'S 1', table_name 'T 4');
 
+CREATE FOREIGN TABLE ft8 (
+	c1 int4range NOT NULL,
+	c2 int NOT NULL,
+	c3 text,
+	c4 daterange NOT NULL
+) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5');
+
+
 -- ===================================================================
 -- tests for validator
 -- ===================================================================
@@ -1546,6 +1569,17 @@ EXPLAIN (verbose, costs off)
 DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass;                       -- can be pushed down
 DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass;
 
+-- Test UPDATE FOR PORTION OF
+UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01'
+SET c2 = c2 + 1
+WHERE c1 = '[1,2)';
+SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4;
+
+-- Test DELETE FOR PORTION OF
+DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01'
+WHERE c1 = '[2,3)';
+SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4;
+
 -- Test UPDATE/DELETE with RETURNING on a three-table join
 INSERT INTO ft2 (c1,c2,c3)
   SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id;
diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out
index bcd1f74b2bc..6819812e806 100644
--- a/contrib/test_decoding/expected/ddl.out
+++ b/contrib/test_decoding/expected/ddl.out
@@ -192,6 +192,58 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
  COMMIT
 (33 rows)
 
+-- FOR PORTION OF setup
+CREATE TABLE replication_example_temporal(id int4range, valid_at daterange, somedata int, text varchar(120), PRIMARY KEY (id, valid_at WITHOUT OVERLAPS));
+INSERT INTO replication_example_temporal VALUES ('[1,2)', '[2000-01-01,2020-01-01)', 1, 'aaa');
+INSERT INTO replication_example_temporal VALUES ('[2,3)', '[2000-01-01,2020-01-01)', 1, 'aaa');
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+                                                                                   data                                                                                   
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ BEGIN
+ table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa'
+ COMMIT
+ BEGIN
+ table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa'
+ COMMIT
+(6 rows)
+
+-- UPDATE FOR PORTION OF support
+BEGIN;
+  UPDATE replication_example_temporal
+    FOR PORTION OF valid_at FROM '2010-01-01' TO '2011-01-01'
+    SET somedata = 2,
+        text = 'bbb'
+    WHERE id = '[1,2)';
+COMMIT;
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+                                                                                                                               data                                                                                                                               
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ BEGIN
+ table public.replication_example_temporal: UPDATE: old-key: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2020)' new-tuple: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2010,01-01-2011)' somedata[integer]:2 text[character varying]:'bbb'
+ table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2010)' somedata[integer]:1 text[character varying]:'aaa'
+ table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2011,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa'
+ COMMIT
+(5 rows)
+
+-- DELETE FOR PORTION OF support
+BEGIN;
+  DELETE FROM replication_example_temporal
+    FOR PORTION OF valid_at FROM '2012-01-01' TO '2013-01-01'
+    WHERE id = '[2,3)';
+COMMIT;
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+                                                                                   data                                                                                   
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ BEGIN
+ table public.replication_example_temporal: DELETE: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2020)'
+ table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2012)' somedata[integer]:1 text[character varying]:'aaa'
+ table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2013,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa'
+ COMMIT
+(5 rows)
+
 -- MERGE support
 BEGIN;
 MERGE INTO replication_example t
diff --git a/contrib/test_decoding/sql/ddl.sql b/contrib/test_decoding/sql/ddl.sql
index 2f8e4e7f2cc..6d0b7d77778 100644
--- a/contrib/test_decoding/sql/ddl.sql
+++ b/contrib/test_decoding/sql/ddl.sql
@@ -93,6 +93,36 @@ COMMIT;
 /* display results */
 SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
 
+-- FOR PORTION OF setup
+CREATE TABLE replication_example_temporal(id int4range, valid_at daterange, somedata int, text varchar(120), PRIMARY KEY (id, valid_at WITHOUT OVERLAPS));
+INSERT INTO replication_example_temporal VALUES ('[1,2)', '[2000-01-01,2020-01-01)', 1, 'aaa');
+INSERT INTO replication_example_temporal VALUES ('[2,3)', '[2000-01-01,2020-01-01)', 1, 'aaa');
+
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+
+-- UPDATE FOR PORTION OF support
+BEGIN;
+  UPDATE replication_example_temporal
+    FOR PORTION OF valid_at FROM '2010-01-01' TO '2011-01-01'
+    SET somedata = 2,
+        text = 'bbb'
+    WHERE id = '[1,2)';
+COMMIT;
+
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+
+-- DELETE FOR PORTION OF support
+BEGIN;
+  DELETE FROM replication_example_temporal
+    FOR PORTION OF valid_at FROM '2012-01-01' TO '2013-01-01'
+    WHERE id = '[2,3)';
+COMMIT;
+
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+
 -- MERGE support
 BEGIN;
 MERGE INTO replication_example t
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
index cd348d5773a..08c0e759719 100644
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -261,6 +261,145 @@ DELETE FROM products;
   </para>
  </sect1>
 
+ <sect1 id="dml-application-time-update-delete">
+  <title>Updating and Deleting Temporal Data</title>
+
+  <para>
+   Special syntax is available to update and delete from <link
+   linkend="ddl-application-time">application-time temporal tables</link>. (No
+   extra syntax is required to insert into them: the user just
+   provides the application time like any other attribute.) When updating
+   or deleting, the user can target a specific portion of history. Only
+   rows overlapping that history are affected, and within those rows only
+   the targeted history is changed. If a row contains more history beyond
+   what is targeted, its application time is reduced to fit within the
+   targeted portion, and new rows are inserted to preserve the history
+   that was not targeted.
+  </para>
+
+  <para>
+   Recall the example table from <xref linkend="temporal-entities-figure" />,
+   containing this data:
+
+<programlisting>
+ product_no | price |        valid_at
+------------+-------+-------------------------
+          5 |  5.00 | [2020-01-01,2022-01-01)
+          5 |  8.00 | [2022-01-01,)
+          6 |  9.00 | [2021-01-01,2024-01-01)
+</programlisting>
+
+   A temporal update might look like this:
+
+<programlisting>
+UPDATE products
+  FOR PORTION OF valid_at FROM '2023-09-01' TO '2025-03-01'
+  AS p
+  SET price = 12.00
+  WHERE product_no = 5;
+</programlisting>
+
+   That command will update the second record for product 5. It will set the
+   price to 12.00 and the application time to <literal>[2023-09-01,2025-03-01)</literal>.
+   Then, since the row's application time was originally
+   <literal>[2022-01-01,)</literal>, the command must insert two
+   <glossterm linkend="glossary-temporal-leftovers">temporal
+   leftovers</glossterm>: one for history before September 1, 2023, and
+   another for history since March 1, 2025. After the update, the table
+   has four rows for product 5:
+
+<programlisting>
+ product_no | price |        valid_at
+------------+-------+-------------------------
+          5 |  5.00 | [2020-01-01,2022-01-01)
+          5 |  8.00 | [2022-01-01,2023-09-01)
+          5 | 12.00 | [2023-09-01,2025-03-01)
+          5 |  8.00 | [2025-03-01,)
+</programlisting>
+
+   The new history could be plotted as in <xref linkend="temporal-update-figure"/>.
+  </para>
+
+  <figure id="temporal-update-figure">
+   <title>Temporal Update Example</title>
+   <mediaobject>
+    <imageobject>
+     <imagedata fileref="images/temporal-update.svg" format="SVG" width="100%"/>
+    </imageobject>
+   </mediaobject>
+  </figure>
+
+  <para>
+   Similarly, a specific portion of history may be targeted when
+   deleting rows from a table. In that case, the original rows are
+   removed, but new
+   <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>
+   are inserted to preserve the untouched history. The syntax for a
+   temporal delete is:
+
+<programlisting>
+DELETE FROM products
+  FOR PORTION OF valid_at FROM '2021-08-01' TO '2023-09-01'
+  AS p
+WHERE product_no = 5;
+</programlisting>
+
+   Continuing the example, this command would delete two records. The
+   first record would yield a single temporal leftover, and the second
+   would be deleted entirely. The rows for product 5 would now be:
+
+<programlisting>
+ product_no | price |        valid_at
+------------+-------+-------------------------
+          5 |  5.00 | [2020-01-01,2021-08-01)
+          5 | 12.00 | [2023-09-01,2025-03-01)
+          5 |  8.00 | [2025-03-01,)
+</programlisting>
+
+   The new history could be plotted as in <xref linkend="temporal-delete-figure"/>.
+  </para>
+
+  <figure id="temporal-delete-figure">
+   <title>Temporal Delete Example</title>
+   <mediaobject>
+    <imageobject>
+     <imagedata fileref="images/temporal-delete.svg" format="SVG" width="100%"/>
+    </imageobject>
+   </mediaobject>
+  </figure>
+
+  <para>
+   Instead of using the <literal>FROM ... TO ...</literal> syntax,
+   temporal update/delete commands can also give the targeted
+   range/multirange directly, inside parentheses. For example:
+   <literal>DELETE FROM products FOR PORTION OF valid_at ('[2028-01-01,)') ...</literal>.
+   This syntax is required when application time is stored
+   in a multirange column.
+  </para>
+
+  <para>
+   When application time is stored in a rangetype column, zero, one or
+   two temporal leftovers are produced by each row that is
+   updated/deleted. With a multirange column, only zero or one temporal
+   leftover is produced. The leftover bounds are computed using
+   <literal>range_minus_multi</literal> and
+   <literal>multirange_minus_multi</literal>
+   (see <xref linkend="functions-range"/>).
+  </para>
+
+  <para>
+   The bounds given to <literal>FOR PORTION OF</literal> must be
+   constant. Functions like <literal>NOW()</literal> are allowed, but
+   column references are not.
+  </para>
+
+  <para>
+   When temporal leftovers are inserted, all <literal>INSERT</literal>
+   triggers are fired, but permission checks for inserting rows are
+   skipped.
+  </para>
+ </sect1>
+
  <sect1 id="dml-returning">
   <title>Returning Data from Modified Rows</title>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..113d7640626 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1933,6 +1933,21 @@
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-temporal-leftovers">
+   <glossterm>Temporal leftovers</glossterm>
+   <glossdef>
+    <para>
+     After a temporal update or delete, the portion of history that was not
+     updated/deleted. When using ranges to track application time, there may be
+     zero, one, or two stretches of history that were not updated/deleted
+     (before and/or after the portion that was updated/deleted). New rows are
+     automatically inserted into the table to preserve that history. A single
+     multirange can accommodate the untouched history before and after the
+     update/delete, so there will be only zero or one leftover.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-temporal-table">
    <glossterm>Temporal table</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..38f8869d78d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -7,7 +7,9 @@ ALL_IMAGES = \
 	gin.svg \
 	pagelayout.svg \
 	temporal-entities.svg \
-	temporal-references.svg
+	temporal-references.svg \
+	temporal-update.svg \
+	temporal-delete.svg
 
 DITAA = ditaa
 DOT = dot
diff --git a/doc/src/sgml/images/temporal-delete.svg b/doc/src/sgml/images/temporal-delete.svg
new file mode 100644
index 00000000000..2d8b1d6ec7b
--- /dev/null
+++ b/doc/src/sgml/images/temporal-delete.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1330 224" width="1330" height="224" shape-rendering="geometricPrecision" version="1.0">
+  <defs>
+    <filter id="f2" x="0" y="0" width="200%" height="200%">
+      <feOffset result="offOut" in="SourceGraphic" dx="5" dy="5"/>
+      <feGaussianBlur result="blurOut" in="offOut" stdDeviation="3"/>
+      <feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
+    </filter>
+  </defs>
+  <g stroke-width="1" stroke-linecap="square" stroke-linejoin="round">
+    <rect x="0" y="0" width="1330" height="224" style="fill: #ffffff"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M685.0 63.0 L685.0 147.0 L1005.0 147.0 L1005.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M315.0 63.0 L315.0 147.0 L25.0 147.0 L25.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M1005.0 63.0 L1005.0 147.0 L1275.0 147.0 L1275.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M205.0 168.0 L205.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M25.0 168.0 L25.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M385.0 168.0 L385.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M565.0 168.0 L565.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M745.0 168.0 L745.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M925.0 168.0 L925.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M1105.0 168.0 L1105.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M1285.0 168.0 L1285.0 181.0 "/>
+    <text x="46" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="40" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 5.00,</text>
+    <text x="83" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Jan 2020,1 Aug 2021))</text>
+    <text x="20" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2020</text>
+    <text x="200" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2021</text>
+    <text x="380" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2022</text>
+    <text x="560" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2023</text>
+    <text x="706" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="700" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 12.00,</text>
+    <text x="743" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Sep 2023,1 Mar 2025))</text>
+    <text x="740" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2024</text>
+    <text x="920" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2025</text>
+    <text x="1026" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="1020" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 8.00,</text>
+    <text x="1056" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Mar 2025,))</text>
+    <text x="1100" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2026</text>
+    <text x="1289" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">...</text>
+  </g>
+</svg>
diff --git a/doc/src/sgml/images/temporal-delete.txt b/doc/src/sgml/images/temporal-delete.txt
new file mode 100644
index 00000000000..bf79b2207c3
--- /dev/null
+++ b/doc/src/sgml/images/temporal-delete.txt
@@ -0,0 +1,10 @@
++----------------------------+                                    +-------------------------------+--------------------------+
+| cGRE                       |                                    | cGRE                          | cGRE                     |
+| products                   |                                    | products                      | products                 |
+| (5, 5.00,                  |                                    | (5, 12.00,                    | (5, 8.00,                |
+|   [1 Jan 2020,1 Aug 2021)) |                                    |   [1 Sep 2023,1 Mar 2025))    |   [1 Mar 2025,))         |
+|                            |                                    |                               |                          |
++----------------------------+                                    +-------------------------------+--------------------------+
+
+|                 |                 |                 |                 |                 |                 |                 |
+2020              2021              2022              2023              2024              2025              2026              ...
diff --git a/doc/src/sgml/images/temporal-update.svg b/doc/src/sgml/images/temporal-update.svg
new file mode 100644
index 00000000000..6c7c43c8d22
--- /dev/null
+++ b/doc/src/sgml/images/temporal-update.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1330 224" width="1330" height="224" shape-rendering="geometricPrecision" version="1.0">
+  <defs>
+    <filter id="f2" x="0" y="0" width="200%" height="200%">
+      <feOffset result="offOut" in="SourceGraphic" dx="5" dy="5"/>
+      <feGaussianBlur result="blurOut" in="offOut" stdDeviation="3"/>
+      <feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
+    </filter>
+  </defs>
+  <g stroke-width="1" stroke-linecap="square" stroke-linejoin="round">
+    <rect x="0" y="0" width="1330" height="224" style="fill: #ffffff"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M385.0 63.0 L385.0 147.0 L25.0 147.0 L25.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M1285.0 63.0 L1285.0 147.0 L975.0 147.0 L975.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M385.0 147.0 L685.0 147.0 L685.0 63.0 L385.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M685.0 63.0 L685.0 147.0 L975.0 147.0 L975.0 63.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M205.0 168.0 L205.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M25.0 168.0 L25.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M385.0 168.0 L385.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M565.0 168.0 L565.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M745.0 168.0 L745.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M925.0 168.0 L925.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M1105.0 168.0 L1105.0 181.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M1285.0 168.0 L1285.0 181.0 "/>
+    <text x="46" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="40" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 5.00,</text>
+    <text x="86" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Jan 2020,1 Jan 2022))</text>
+    <text x="20" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2020</text>
+    <text x="200" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2021</text>
+    <text x="406" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="400" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 8.00,</text>
+    <text x="445" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Jan 2022,1 Sep 2023))</text>
+    <text x="380" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2022</text>
+    <text x="560" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2023</text>
+    <text x="706" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="700" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 12.00,</text>
+    <text x="743" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Sep 2023,1 Mar 2025))</text>
+    <text x="740" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2024</text>
+    <text x="920" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2025</text>
+    <text x="996" y="96" font-family="Courier" font-size="15" stroke="none" fill="#000000">products</text>
+    <text x="990" y="110" font-family="Courier" font-size="15" stroke="none" fill="#000000">(5, 8.00,</text>
+    <text x="1026" y="124" font-family="Courier" font-size="15" stroke="none" fill="#000000">[1 Mar 2025,))</text>
+    <text x="1100" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">2026</text>
+    <text x="1289" y="194" font-family="Courier" font-size="15" stroke="none" fill="#000000">...</text>
+  </g>
+</svg>
diff --git a/doc/src/sgml/images/temporal-update.txt b/doc/src/sgml/images/temporal-update.txt
new file mode 100644
index 00000000000..87a16382810
--- /dev/null
+++ b/doc/src/sgml/images/temporal-update.txt
@@ -0,0 +1,10 @@
++-----------------------------------+-----------------------------+----------------------------+------------------------------+
+| cGRE                              | cGRE                        | cGRE                       | cGRE                         |
+| products                          | products                    | products                   | products                     |
+| (5, 5.00,                         | (5, 8.00,                   | (5, 12.00,                 | (5, 8.00,                    |
+|   [1 Jan 2020,1 Jan 2022))        |   [1 Jan 2022,1 Sep 2023))  |   [1 Sep 2023,1 Mar 2025)) |   [1 Mar 2025,))             |
+|                                   |                             |                            |                              |
++-----------------------------------+-----------------------------+----------------------------+------------------------------+
+
+|                 |                 |                 |                 |                 |                 |                 |
+2020              2021              2022              2023              2024              2025              2026              ...
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 6efbb915cec..48b10db0d41 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -396,6 +396,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    for each row inserted, updated, or deleted.
   </para>
 
+  <para>
+   For an <command>UPDATE/DELETE ... FOR PORTION OF</command> command, the
+   publication will publish an <command>UPDATE</command> or <command>DELETE</command>,
+   followed by one <command>INSERT</command> for each temporal leftover row inserted.
+  </para>
+
   <para>
    <command>ATTACH</command>ing a table into a partition tree whose root is
    published using a publication with <literal>publish_via_partition_root</literal>
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index b9367f2b23c..c22e7e88e28 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -22,11 +22,18 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
-DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+    [ FOR PORTION OF <replaceable class="parameter">range_column_name</replaceable> <replaceable class="parameter">for_portion_of_target</replaceable> ]
+    [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
     [ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
     [ RETURNING [ WITH ( { OLD | NEW } AS <replaceable class="parameter">output_alias</replaceable> [, ...] ) ]
                 { * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
+
+<phrase>where <replaceable class="parameter">for_portion_of_target</replaceable> is:</phrase>
+
+{ FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> |
+  ( <replaceable class="parameter">portion</replaceable> ) }
 </synopsis>
  </refsynopsisdiv>
 
@@ -55,6 +62,49 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
+   If the table has a range or multirange column,
+   you may supply a <literal>FOR PORTION OF</literal> clause, and the delete will
+   only affect rows that overlap the given portion. Furthermore, if a row's
+   application time extends outside the <literal>FOR PORTION OF</literal> bounds,
+   then the delete will only change the application time within those bounds.
+   In effect you are deleting the history targeted by <literal>FOR PORTION OF</literal>
+   and no moments outside.
+  </para>
+
+  <para>
+   Specifically, after <productname>PostgreSQL</productname> deletes a row,
+   it will <literal>INSERT</literal>
+   new <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>:
+   rows whose range or multirange receives the remaining application time outside
+   the targeted <literal>FROM</literal>/<literal>TO</literal> bounds, with the
+   original values in their other columns. For range columns, there will be zero
+   to two inserted records, depending on whether the original application time was
+   completely deleted, extended before/after the change, or both. For
+   instance given an original range of <literal>[2,6)</literal>, a delete of
+   <literal>[1,7)</literal> yields no leftovers, a delete of
+   <literal>[2,5)</literal> yields one, and a delete of
+   <literal>[3,5)</literal> yields two. Multiranges never require two temporal
+   leftovers, because one value can always contain whatever application time remains.
+  </para>
+
+  <para>
+   These secondary inserts fire <literal>INSERT</literal> triggers.
+   Both <literal>STATEMENT</literal> and <literal>ROW</literal> triggers are fired.
+   The <literal>BEFORE DELETE</literal> triggers are fired first, then
+   <literal>BEFORE INSERT</literal>, then <literal>AFTER INSERT</literal>,
+   then <literal>AFTER DELETE</literal>.
+  </para>
+
+  <para>
+   These secondary inserts do not require <literal>INSERT</literal> privilege on
+   the table. This is because conceptually no new information has been added.
+   The inserted rows only preserve existing data about the untargeted time period.
+   Note this may result in users firing <literal>INSERT</literal> triggers who
+   don't have insert privileges, so be careful about <literal>SECURITY DEFINER</literal>
+   trigger functions!
+  </para>
+
   <para>
    The optional <literal>RETURNING</literal> clause causes <command>DELETE</command>
    to compute and return value(s) based on each row actually deleted.
@@ -117,6 +167,58 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">range_column_name</replaceable></term>
+    <listitem>
+     <para>
+      The range or multirange column to use when performing a temporal delete.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">for_portion_of_target</replaceable></term>
+    <listitem>
+     <para>
+      The portion to delete. If you are targeting a range column,
+      you may give this in the form <literal>FROM</literal>
+      <replaceable class="parameter">start_time</replaceable> <literal>TO</literal>
+      <replaceable class="parameter">end_time</replaceable>.
+      Otherwise you must use
+      <literal>(</literal><replaceable class="parameter">portion</replaceable><literal>)</literal>
+      where <replaceable class="parameter">portion</replaceable> is an expression
+      that yields a value of the same type as
+      <replaceable class="parameter">range_column_name</replaceable>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">start_time</replaceable></term>
+    <listitem>
+     <para>
+      The earliest time (inclusive) to change in a temporal delete.
+      This must be a value matching the base type of the range from
+      <replaceable class="parameter">range_column_name</replaceable>. A
+      <literal>NULL</literal> here indicates a delete whose beginning is
+      unbounded (as with range types).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">end_time</replaceable></term>
+    <listitem>
+     <para>
+      The latest time (exclusive) to change in a temporal delete.
+      This must be a value matching the base type of the range from
+      <replaceable class="parameter">range_column_name</replaceable>. A
+      <literal>NULL</literal> here indicates a delete whose end is unbounded
+      (as with range types).
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><replaceable class="parameter">from_item</replaceable></term>
     <listitem>
@@ -238,6 +340,10 @@ DELETE <replaceable class="parameter">count</replaceable>
    suppressed by a <literal>BEFORE DELETE</literal> trigger.  If <replaceable
    class="parameter">count</replaceable> is 0, no rows were deleted by
    the query (this is not considered an error).
+   If <literal>FOR PORTION OF</literal> was used, the
+   <replaceable class="parameter">count</replaceable> does not include
+   <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>
+   that were inserted.
   </para>
 
   <para>
@@ -245,7 +351,13 @@ DELETE <replaceable class="parameter">count</replaceable>
    clause, the result will be similar to that of a <command>SELECT</command>
    statement containing the columns and values defined in the
    <literal>RETURNING</literal> list, computed over the row(s) deleted by the
-   command.
+   command. If <literal>FOR PORTION OF</literal> was used, the
+   <literal>RETURNING</literal> clause gives one result for each deleted row,
+   but does not include inserted
+   <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>.
+   The value of the application-time column matches the old value of the deleted
+   row(s). Note this will represent more application time than was actually erased,
+   if temporal leftovers were inserted.
   </para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index b523766abe3..3feb7ee046e 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -22,7 +22,9 @@ PostgreSQL documentation
  <refsynopsisdiv>
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
-UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
+    [ FOR PORTION OF <replaceable class="parameter">range_column_name</replaceable> <replaceable class="parameter">for_portion_of_target</replaceable> ]
+    [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
     SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
           ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) |
           ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable class="parameter">sub-SELECT</replaceable> )
@@ -31,6 +33,11 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     [ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
     [ RETURNING [ WITH ( { OLD | NEW } AS <replaceable class="parameter">output_alias</replaceable> [, ...] ) ]
                 { * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
+
+<phrase>where <replaceable class="parameter">for_portion_of_target</replaceable> is:</phrase>
+
+{ FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> |
+  ( <replaceable class="parameter">portion</replaceable> ) }
 </synopsis>
  </refsynopsisdiv>
 
@@ -52,6 +59,51 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
+   If the table has a range or multirange column,
+   you may supply a <literal>FOR PORTION OF</literal> clause, and the update will
+   only affect rows that overlap the given portion. Furthermore, if a row's
+   application time extends outside the <literal>FOR PORTION OF</literal> bounds,
+   then the update will only change the application time within those bounds.
+   In effect you are updating the history targeted by <literal>FOR PORTION OF</literal>
+   and no moments outside.
+  </para>
+
+  <para>
+   Specifically, when <productname>PostgreSQL</productname> updates a row,
+   it will first shrink the range or multirange so that its application time
+   no longer extends beyond the targeted <literal>FOR PORTION OF</literal> bounds.
+   Then <productname>PostgreSQL</productname> will <literal>INSERT</literal>
+   new <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>:
+   rows whose range or multirange receives the remaining application time outside
+   the targeted <literal>FROM</literal>/<literal>TO</literal> bounds, with the
+   original values in their other columns. For range columns, there will be zero
+   to two inserted records, depending on whether the original application time was
+   completely updated, extended before/after the change, or both. For
+   instance given an original range of <literal>[2,6)</literal>, an update of
+   <literal>[1,7)</literal> yields no leftovers, an update of
+   <literal>[2,5)</literal> yields one, and an update of
+   <literal>[3,5)</literal> yields two. Multiranges never require two temporal
+   leftovers, because one value can always contain whatever application time remains.
+  </para>
+
+  <para>
+   These secondary inserts fire <literal>INSERT</literal> triggers.
+   Both <literal>STATEMENT</literal> and <literal>ROW</literal> triggers are fired.
+   The <literal>BEFORE UPDATE</literal> triggers are fired first, then
+   <literal>BEFORE INSERT</literal>, then <literal>AFTER INSERT</literal>,
+   then <literal>AFTER UPDATE</literal>.
+  </para>
+
+  <para>
+   These secondary inserts do not require <literal>INSERT</literal> privilege on
+   the table. This is because conceptually no new information has been added.
+   The inserted rows only preserve existing data about the untargeted time period.
+   Note this may result in users firing <literal>INSERT</literal> triggers who
+   don't have insert privileges, so be careful about <literal>SECURITY DEFINER</literal>
+   trigger functions!
+  </para>
+
   <para>
    The optional <literal>RETURNING</literal> clause causes <command>UPDATE</command>
    to compute and return value(s) based on each row actually updated.
@@ -116,6 +168,58 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">range_column_name</replaceable></term>
+    <listitem>
+     <para>
+      The range or multirange column to use when performing a temporal update.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">for_portion_of_target</replaceable></term>
+    <listitem>
+     <para>
+      The portion to update. If you are targeting a range column,
+      you may give this in the form <literal>FROM</literal>
+      <replaceable class="parameter">start_time</replaceable> <literal>TO</literal>
+      <replaceable class="parameter">end_time</replaceable>.
+      Otherwise you must use
+      <literal>(</literal><replaceable class="parameter">portion</replaceable><literal>)</literal>
+      where <replaceable class="parameter">portion</replaceable> is an expression
+      that yields a value of the same type as
+      <replaceable class="parameter">range_column_name</replaceable>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">start_time</replaceable></term>
+    <listitem>
+     <para>
+      The earliest time (inclusive) to change in a temporal update.
+      This must be a value matching the base type of the range from
+      <replaceable class="parameter">range_column_name</replaceable>. A
+      <literal>NULL</literal> here indicates an update whose beginning is
+      unbounded (as with range types).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">end_time</replaceable></term>
+    <listitem>
+     <para>
+      The latest time (exclusive) to change in a temporal update.
+      This must be a value matching the base type of the range from
+      <replaceable class="parameter">range_column_name</replaceable>. A
+      <literal>NULL</literal> here indicates an update whose end is unbounded
+      (as with range types).
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><replaceable class="parameter">column_name</replaceable></term>
     <listitem>
@@ -283,6 +387,10 @@ UPDATE <replaceable class="parameter">count</replaceable>
    updates were suppressed by a <literal>BEFORE UPDATE</literal> trigger.  If
    <replaceable class="parameter">count</replaceable> is 0, no rows were
    updated by the query (this is not considered an error).
+   If <literal>FOR PORTION OF</literal> was used, the
+   <replaceable class="parameter">count</replaceable> does not include
+   <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>
+   that were inserted.
   </para>
 
   <para>
@@ -290,7 +398,12 @@ UPDATE <replaceable class="parameter">count</replaceable>
    clause, the result will be similar to that of a <command>SELECT</command>
    statement containing the columns and values defined in the
    <literal>RETURNING</literal> list, computed over the row(s) updated by the
-   command.
+   command. If <literal>FOR PORTION OF</literal> was used, the
+   <literal>RETURNING</literal> clause gives one result for each updated row,
+   but does not include inserted
+   <glossterm linkend="glossary-temporal-leftovers">temporal leftovers</glossterm>.
+   The value of the application-time column matches the new value of the updated
+   row(s).
   </para>
  </refsect1>
 
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index 0062f1a3fd1..2b68c3882ec 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -373,6 +373,15 @@
     responsibility to avoid that.
    </para>
 
+   <para>
+    If an <command>UPDATE</command> or <command>DELETE</command> uses
+    <literal>FOR PORTION OF</literal>, causing new rows to be inserted
+    to preserve the leftover untargeted part of modified records, then
+    <command>INSERT</command> triggers are fired for each inserted
+    row. Each row is inserted separately, so they fire their own
+    statement triggers, and they have their own transition tables.
+   </para>
+
    <para>
     <indexterm>
      <primary>trigger</primary>
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index bfd3ebc601e..8ce6fd17248 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1299,6 +1299,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_projectReturning = NULL;
 	resultRelInfo->ri_onConflictArbiterIndexes = NIL;
 	resultRelInfo->ri_onConflict = NULL;
+	resultRelInfo->ri_forPortionOf = NULL;
 	resultRelInfo->ri_ReturningSlot = NULL;
 	resultRelInfo->ri_TrigOldSlot = NULL;
 	resultRelInfo->ri_TrigNewSlot = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6802fc13e95..38a0901b658 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -69,6 +69,7 @@
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/injection_point.h"
+#include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
@@ -132,7 +133,6 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
-
 static void ExecBatchInsert(ModifyTableState *mtstate,
 							ResultRelInfo *resultRelInfo,
 							TupleTableSlot **slots,
@@ -165,6 +165,10 @@ static bool ExecOnConflictSelect(ModifyTableContext *context,
 								 TupleTableSlot *excludedSlot,
 								 bool canSetTag,
 								 TupleTableSlot **returning);
+static void ExecForPortionOfLeftovers(ModifyTableContext *context,
+									  EState *estate,
+									  ResultRelInfo *resultRelInfo,
+									  ItemPointer tupleid);
 static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
 											   EState *estate,
 											   PartitionTupleRouting *proute,
@@ -187,6 +191,9 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
 static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
 										   ResultRelInfo *resultRelInfo,
 										   bool canSetTag);
+static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate);
+static void fireBSTriggers(ModifyTableState *node);
+static void fireASTriggers(ModifyTableState *node);

 /*
@@ -1384,6 +1391,235 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+/* ----------------------------------------------------------------
+ *		ExecForPortionOfLeftovers
+ *
+ *		Insert tuples for the untouched portion of a row in a FOR
+ *		PORTION OF UPDATE/DELETE
+ * ----------------------------------------------------------------
+ */
+static void
+ExecForPortionOfLeftovers(ModifyTableContext *context,
+						  EState *estate,
+						  ResultRelInfo *resultRelInfo,
+						  ItemPointer tupleid)
+{
+	ModifyTableState *mtstate = context->mtstate;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+	AttrNumber	rangeAttno;
+	Datum		oldRange;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState;
+	TupleTableSlot *oldtupleSlot;
+	TupleTableSlot *leftoverSlot;
+	TupleConversionMap *map = NULL;
+	HeapTuple	oldtuple = NULL;
+	CmdType		oldOperation;
+	TransitionCaptureState *oldTcs;
+	FmgrInfo	flinfo;
+	ReturnSetInfo rsi;
+	bool		didInit = false;
+	bool		shouldFree = false;
+
+	LOCAL_FCINFO(fcinfo, 2);
+
+	if (!resultRelInfo->ri_forPortionOf)
+	{
+		/*
+		 * If we don't have a ForPortionOfState yet, we must be a partition
+		 * child being hit for the first time. Make a copy from the root, with
+		 * our own tupleTableSlot. We do this lazily so that we don't pay the
+		 * price of unused partitions.
+		 */
+		ForPortionOfState *leafState = makeNode(ForPortionOfState);
+
+		if (!mtstate->rootResultRelInfo)
+			elog(ERROR, "no root relation but ri_forPortionOf is uninitialized");
+
+		fpoState = mtstate->rootResultRelInfo->ri_forPortionOf;
+		Assert(fpoState);
+
+		leafState->fp_rangeName = fpoState->fp_rangeName;
+		leafState->fp_rangeType = fpoState->fp_rangeType;
+		leafState->fp_rangeAttno = fpoState->fp_rangeAttno;
+		leafState->fp_targetRange = fpoState->fp_targetRange;
+		leafState->fp_Leftover = fpoState->fp_Leftover;
+		/* Each partition needs a slot matching its tuple descriptor */
+		leafState->fp_Existing =
+			table_slot_create(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		resultRelInfo->ri_forPortionOf = leafState;
+	}
+	fpoState = resultRelInfo->ri_forPortionOf;
+	oldtupleSlot = fpoState->fp_Existing;
+	leftoverSlot = fpoState->fp_Leftover;
+
+	/*
+	 * Get the old pre-UPDATE/DELETE tuple. We will use its range to compute
+	 * untouched parts of history, and if necessary we will insert copies with
+	 * truncated start/end times.
+	 *
+	 * We have already locked the tuple in ExecUpdate/ExecDelete, and it has
+	 * passed EvalPlanQual. This ensures that concurrent updates in READ
+	 * COMMITTED can't insert conflicting temporal leftovers.
+	 */
+	if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot))
+		elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
+
+	/*
+	 * Get the old range of the record being updated/deleted. Must read with
+	 * the attno of the leaf partition being updated.
+	 */
+
+	rangeAttno = forPortionOf->rangeVar->varattno;
+	if (resultRelInfo->ri_RootResultRelInfo)
+		map = ExecGetChildToRootMap(resultRelInfo);
+	if (map != NULL)
+		rangeAttno = map->attrMap->attnums[rangeAttno - 1];
+	slot_getallattrs(oldtupleSlot);
+
+	if (oldtupleSlot->tts_isnull[rangeAttno - 1])
+		elog(ERROR, "found a NULL range in a temporal table");
+	oldRange = oldtupleSlot->tts_values[rangeAttno - 1];
+
+	/*
+	 * Get the range's type cache entry. This is worth caching for the whole
+	 * UPDATE/DELETE as range functions do.
+	 */
+
+	typcache = fpoState->fp_leftoverstypcache;
+	if (typcache == NULL)
+	{
+		typcache = lookup_type_cache(forPortionOf->rangeType, 0);
+		fpoState->fp_leftoverstypcache = typcache;
+	}
+
+	/*
+	 * Get the ranges to the left/right of the targeted range. We call a SETOF
+	 * support function and insert as many temporal leftovers as it gives us.
+	 * Although rangetypes have 0/1/2 leftovers, multiranges have 0/1, and
+	 * other types may have more.
+	 */
+
+	fmgr_info(forPortionOf->withoutPortionProc, &flinfo);
+	rsi.type = T_ReturnSetInfo;
+	rsi.econtext = mtstate->ps.ps_ExprContext;
+	rsi.expectedDesc = NULL;
+	rsi.allowedModes = (int) (SFRM_ValuePerCall);
+	rsi.returnMode = SFRM_ValuePerCall;
+	rsi.setResult = NULL;
+	rsi.setDesc = NULL;
+
+	InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, (Node *) &rsi);
+	fcinfo->args[0].value = oldRange;
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = fpoState->fp_targetRange;
+	fcinfo->args[1].isnull = false;
+
+	/*
+	 * If there are partitions, we must insert into the root table, so we get
+	 * tuple routing. We already set up leftoverSlot with the root tuple
+	 * descriptor.
+	 */
+	if (resultRelInfo->ri_RootResultRelInfo)
+		resultRelInfo = resultRelInfo->ri_RootResultRelInfo;
+
+	/*
+	 * Insert a leftover for each value returned by the without_portion helper
+	 * function
+	 */
+	while (true)
+	{
+		Datum		leftover = FunctionCallInvoke(fcinfo);
+
+		/* Are we done? */
+		if (rsi.isDone == ExprEndResult)
+			break;
+
+		if (fcinfo->isnull)
+			elog(ERROR, "Got a null from without_portion function");
+
+		/*
+		 * Does the new Datum violate domain checks? Row-level CHECK
+		 * constraints are validated by ExecInsert, so we don't need to do
+		 * anything here for those.
+		 */
+		if (forPortionOf->isDomain)
+			domain_check(leftover, false, forPortionOf->rangeVar->vartype, NULL, NULL);
+
+		if (!didInit)
+		{
+			/*
+			 * Make a copy of the pre-UPDATE row. Then we'll overwrite the
+			 * range column below. Convert oldtuple to the base table's format
+			 * if necessary. We need to insert temporal leftovers through the
+			 * root partition so they get routed correctly.
+			 */
+			if (map != NULL)
+			{
+				leftoverSlot = execute_attr_map_slot(map->attrMap,
+													 oldtupleSlot,
+													 leftoverSlot);
+			}
+			else
+			{
+				oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, &shouldFree);
+				ExecForceStoreHeapTuple(oldtuple, leftoverSlot, false);
+			}
+
+			/*
+			 * Save some mtstate things so we can restore them below. XXX:
+			 * Should we create our own ModifyTableState instead?
+			 */
+			oldOperation = mtstate->operation;
+			mtstate->operation = CMD_INSERT;
+			oldTcs = mtstate->mt_transition_capture;
+
+			didInit = true;
+		}
+
+		leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+		leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+		ExecMaterializeSlot(leftoverSlot);
+
+		/*
+		 * If there are partitions, we must insert into the root table, so we
+		 * get tuple routing. We already set up leftoverSlot with the root
+		 * tuple descriptor.
+		 */
+		if (resultRelInfo->ri_RootResultRelInfo)
+			resultRelInfo = resultRelInfo->ri_RootResultRelInfo;
+
+		/*
+		 * The standard says that each temporal leftover should execute its
+		 * own INSERT statement, firing all statement and row triggers, but
+		 * skipping insert permission checks. Therefore we give each insert
+		 * its own transition table. If we just push & pop a new trigger level
+		 * for each insert, we get exactly what we need.
+		 *
+		 * We have to make sure that the inserts don't add to the ROW_COUNT
+		 * diagnostic or the command tag, so we pass false for canSetTag.
+		 */
+		AfterTriggerBeginQuery();
+		ExecSetupTransitionCaptureState(mtstate, estate);
+		fireBSTriggers(mtstate);
+		ExecInsert(context, resultRelInfo, leftoverSlot, false, NULL, NULL);
+		fireASTriggers(mtstate);
+		AfterTriggerEndQuery(estate);
+	}
+
+	if (didInit)
+	{
+		mtstate->operation = oldOperation;
+		mtstate->mt_transition_capture = oldTcs;
+
+		if (shouldFree)
+			heap_freetuple(oldtuple);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1537,7 +1773,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
  *
  * Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers,
  * including the UPDATE triggers if the deletion is being done as part of a
- * cross-partition tuple move.
+ * cross-partition tuple move. It also inserts temporal leftovers from a
+ * DELETE FOR PORTION OF.
  */
 static void
 ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1570,6 +1807,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
+	/* Compute temporal leftovers in FOR PORTION OF */
+	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+		ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid);
+
 	/* AFTER ROW DELETE Triggers */
 	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
 						 ar_delete_trig_tcs, changingPart);
@@ -1995,7 +2236,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 	if (resultRelInfo == mtstate->rootResultRelInfo)
 		ExecPartitionCheckEmitError(resultRelInfo, slot, estate);
 
-	/* Initialize tuple routing info if not already done. */
+	/*
+	 * Initialize tuple routing info if not already done. Note whatever we do
+	 * here must be done in ExecInitModifyTable for FOR PORTION OF as well.
+	 */
 	if (mtstate->mt_partition_tuple_routing == NULL)
 	{
 		Relation	rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
@@ -2344,7 +2588,8 @@ lreplace:
  * ExecUpdateEpilogue -- subroutine for ExecUpdate
  *
  * Closing steps of updating a tuple.  Must be called if ExecUpdateAct
- * returns indicating that the tuple was updated.
+ * returns indicating that the tuple was updated. It also inserts temporal
+ * leftovers from an UPDATE FOR PORTION OF.
  */
 static void
 ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
@@ -2362,6 +2607,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL, NIL,
 											   (updateCxt->updateIndexes == TU_Summarizing));
 
+	/* Compute temporal leftovers in FOR PORTION OF */
+	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+		ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(context->estate, resultRelInfo,
 						 NULL, NULL,
@@ -5289,6 +5538,101 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+	 * If needed, initialize the target range for FOR PORTION OF.
+	 */
+	if (node->forPortionOf)
+	{
+		ResultRelInfo *rootRelInfo;
+		TupleDesc	tupDesc;
+		ForPortionOfExpr *forPortionOf;
+		Datum		targetRange;
+		bool		isNull;
+		ExprContext *econtext;
+		ExprState  *exprState;
+		ForPortionOfState *fpoState;
+
+		rootRelInfo = mtstate->resultRelInfo;
+		if (rootRelInfo->ri_RootResultRelInfo)
+			rootRelInfo = rootRelInfo->ri_RootResultRelInfo;
+
+		tupDesc = rootRelInfo->ri_RelationDesc->rd_att;
+		forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+
+		/* Eval the FOR PORTION OF target */
+		if (mtstate->ps.ps_ExprContext == NULL)
+			ExecAssignExprContext(estate, &mtstate->ps);
+		econtext = mtstate->ps.ps_ExprContext;
+
+		exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
+		targetRange = ExecEvalExpr(exprState, econtext, &isNull);
+		if (isNull)
+			elog(ERROR, "got a NULL FOR PORTION OF target");
+
+		/* Create state for FOR PORTION OF operation */
+
+		fpoState = makeNode(ForPortionOfState);
+		fpoState->fp_rangeName = forPortionOf->range_name;
+		fpoState->fp_rangeType = forPortionOf->rangeType;
+		fpoState->fp_rangeAttno = forPortionOf->rangeVar->varattno;
+		fpoState->fp_targetRange = targetRange;
+
+		/* Initialize slot for the existing tuple */
+
+		fpoState->fp_Existing =
+			table_slot_create(rootRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/* Create the tuple slot for INSERTing the temporal leftovers */
+
+		fpoState->fp_Leftover =
+			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+
+		rootRelInfo->ri_forPortionOf = fpoState;
+
+		/*
+		 * Make sure the root relation has the FOR PORTION OF clause too. Each
+		 * partition needs its own TupleTableSlot, since they can have
+		 * different descriptors, so they'll use the root fpoState to
+		 * initialize one if necessary.
+		 */
+		if (node->rootRelation > 0)
+			mtstate->rootResultRelInfo->ri_forPortionOf = fpoState;
+
+		if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+			mtstate->mt_partition_tuple_routing == NULL)
+		{
+			/*
+			 * We will need tuple routing to insert temporal leftovers. Since
+			 * we are initializing things before ExecCrossPartitionUpdate
+			 * runs, we must do everything it needs as well.
+			 */
+			Relation	rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+			MemoryContext oldcxt;
+
+			/* Things built here have to last for the query duration. */
+			oldcxt = MemoryContextSwitchTo(estate->es_query_cxt);
+
+			mtstate->mt_partition_tuple_routing =
+				ExecSetupPartitionTupleRouting(estate, rootRel);
+
+			/*
+			 * Before a partition's tuple can be re-routed, it must first be
+			 * converted to the root's format, so we'll need a slot for
+			 * storing such tuples.
+			 */
+			Assert(mtstate->mt_root_tuple_slot == NULL);
+			mtstate->mt_root_tuple_slot = table_slot_create(rootRel, NULL);
+
+			MemoryContextSwitchTo(oldcxt);
+		}
+
+		/*
+		 * Don't free the ExprContext here because the result must last for
+		 * the whole query.
+		 */
+	}
+
 	/*
 	 * If we have any secondary relations in an UPDATE or DELETE, they need to
 	 * be treated like non-locked relations in SELECT FOR UPDATE, i.e., the
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 199ed27995f..31fecbc804c 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2570,6 +2570,20 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_ForPortionOfExpr:
+			{
+				ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node;
+
+				if (WALK(forPortionOf->targetFrom))
+					return true;
+				if (WALK(forPortionOf->targetTo))
+					return true;
+				if (WALK(forPortionOf->targetRange))
+					return true;
+				if (WALK(forPortionOf->overlapsExpr))
+					return true;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -2718,6 +2732,8 @@ query_tree_walker_impl(Query *query,
 		return true;
 	if (WALK(query->mergeJoinCondition))
 		return true;
+	if (WALK(query->forPortionOf))
+		return true;
 	if (WALK(query->returningList))
 		return true;
 	if (WALK(query->jointree))
@@ -3612,6 +3628,22 @@ expression_tree_mutator_impl(Node *node,
 				return (Node *) newnode;
 			}
 			break;
+		case T_ForPortionOfExpr:
+			{
+				ForPortionOfExpr *fpo = (ForPortionOfExpr *) node;
+				ForPortionOfExpr *newnode;
+
+				FLATCOPY(newnode, fpo, ForPortionOfExpr);
+				MUTATE(newnode->rangeVar, fpo->rangeVar, Var *);
+				MUTATE(newnode->targetFrom, fpo->targetFrom, Node *);
+				MUTATE(newnode->targetTo, fpo->targetTo, Node *);
+				MUTATE(newnode->targetRange, fpo->targetRange, Node *);
+				MUTATE(newnode->overlapsExpr, fpo->overlapsExpr, Node *);
+				MUTATE(newnode->rangeTargetList, fpo->rangeTargetList, List *);
+
+				return (Node *) newnode;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3793,6 +3825,7 @@ query_tree_mutator_impl(Query *query,
 	MUTATE(query->onConflict, query->onConflict, OnConflictExpr *);
 	MUTATE(query->mergeActionList, query->mergeActionList, List *);
 	MUTATE(query->mergeJoinCondition, query->mergeJoinCondition, Node *);
+	MUTATE(query->forPortionOf, query->forPortionOf, ForPortionOfExpr *);
 	MUTATE(query->returningList, query->returningList, List *);
 	MUTATE(query->jointree, query->jointree, FromExpr *);
 	MUTATE(query->setOperations, query->setOperations, Node *);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 21f1988cf22..d1a43486dd4 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -314,7 +314,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict,
 									 List *mergeActionLists, List *mergeJoinConditions,
-									 int epqParam);
+									 ForPortionOfExpr *forPortionOf, int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
 
@@ -2675,6 +2675,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->onconflict,
 							best_path->mergeActionLists,
 							best_path->mergeJoinConditions,
+							best_path->forPortionOf,
 							best_path->epqParam);
 
 	copy_generic_path_info(&plan->plan, &best_path->path);
@@ -7008,7 +7009,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict,
 				 List *mergeActionLists, List *mergeJoinConditions,
-				 int epqParam)
+				 ForPortionOfExpr *forPortionOf, int epqParam)
 {
 	ModifyTable *node = makeNode(ModifyTable);
 	bool		returning_old_or_new = false;
@@ -7081,6 +7082,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 		node->exclRelTlist = onconflict->exclRelTlist;
 	}
 	node->updateColnosLists = updateColnosLists;
+	node->forPortionOf = (Node *) forPortionOf;
 	node->withCheckOptionLists = withCheckOptionLists;
 	node->returningOldAlias = root->parse->returningOldAlias;
 	node->returningNewAlias = root->parse->returningNewAlias;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 006b3281969..504173f69e3 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2202,6 +2202,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 										parse->onConflict,
 										mergeActionLists,
 										mergeJoinConditions,
+										parse->forPortionOf,
 										assign_special_exec_param(root));
 		}
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 9678c20ff1f..5afd1937615 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3645,7 +3645,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 						List *withCheckOptionLists, List *returningLists,
 						List *rowMarks, OnConflictExpr *onconflict,
 						List *mergeActionLists, List *mergeJoinConditions,
-						int epqParam)
+						ForPortionOfExpr *forPortionOf, int epqParam)
 {
 	ModifyTablePath *pathnode = makeNode(ModifyTablePath);
 
@@ -3711,6 +3711,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->returningLists = returningLists;
 	pathnode->rowMarks = rowMarks;
 	pathnode->onconflict = onconflict;
+	pathnode->forPortionOf = forPortionOf;
 	pathnode->epqParam = epqParam;
 	pathnode->mergeActionLists = mergeActionLists;
 	pathnode->mergeJoinConditions = mergeJoinConditions;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 539c16c4f79..56d422c36f5 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,8 +24,11 @@
 
 #include "postgres.h"
 
+#include "access/stratnum.h"
 #include "access/sysattr.h"
 #include "catalog/dependency.h"
+#include "catalog/pg_am.h"
+#include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
@@ -51,7 +54,10 @@
 #include "parser/parsetree.h"
 #include "utils/backend_status.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
@@ -72,6 +78,10 @@ static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt);
 static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
 static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
 												 OnConflictClause *onConflictClause);
+static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate,
+													 int rtindex,
+													 ForPortionOfClause *forPortionOfClause,
+													 bool isUpdate);
 static int	count_rowexpr_columns(ParseState *pstate, Node *expr);
 static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
 								  SelectStmtPassthrough *passthru);
@@ -604,6 +614,12 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
 
+	if (stmt->forPortionOf)
+		qry->forPortionOf = transformForPortionOfClause(pstate,
+														qry->resultRelation,
+														stmt->forPortionOf,
+														false);
+
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
@@ -1247,7 +1263,7 @@ transformOnConflictClause(ParseState *pstate,
 		/* Process the UPDATE SET clause */
 		if (onConflictClause->action == ONCONFLICT_UPDATE)
 			onConflictSet =
-				transformUpdateTargetList(pstate, onConflictClause->targetList);
+				transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
 
 		/* Process the SELECT/UPDATE WHERE clause */
 		onConflictWhere = transformWhereClause(pstate,
@@ -1279,6 +1295,321 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+ * transformForPortionOfClause
+ *
+ *	  Transforms a ForPortionOfClause in an UPDATE/DELETE statement.
+ *
+ *	  - Look up the range/period requested.
+ *	  - Build a compatible range value from the FROM and TO expressions.
+ *	  - Build an "overlaps" expression for filtering, used later by the
+ *		rewriter.
+ *	  - For UPDATEs, build an "intersects" expression the rewriter can add
+ *		to the targetList to change the temporal bounds.
+ */
+static ForPortionOfExpr *
+transformForPortionOfClause(ParseState *pstate,
+							int rtindex,
+							ForPortionOfClause *forPortionOf,
+							bool isUpdate)
+{
+	Relation	targetrel = pstate->p_target_relation;
+	char	   *range_name = forPortionOf->range_name;
+	int			range_attno = InvalidAttrNumber;
+	Form_pg_attribute attr;
+	Oid			attbasetype;
+	Oid			opclass;
+	Oid			opfamily;
+	Oid			opcintype;
+	Oid			funcid = InvalidOid;
+	StrategyNumber strat;
+	Oid			opid;
+	OpExpr	   *op;
+	ForPortionOfExpr *result;
+	Var		   *rangeVar;
+
+	/* We don't support FOR PORTION OF FDW queries. */
+	if (targetrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("foreign tables don't support FOR PORTION OF")));
+
+	result = makeNode(ForPortionOfExpr);
+
+	/* Look up the FOR PORTION OF name requested. */
+	range_attno = attnameAttNum(targetrel, range_name, false);
+	if (range_attno == InvalidAttrNumber)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column or period \"%s\" of relation \"%s\" does not exist",
+						range_name,
+						RelationGetRelationName(targetrel)),
+				 parser_errposition(pstate, forPortionOf->location)));
+	attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+
+	attbasetype = getBaseType(attr->atttypid);
+
+	rangeVar = makeVar(
+					   rtindex,
+					   range_attno,
+					   attr->atttypid,
+					   attr->atttypmod,
+					   attr->attcollation,
+					   0);
+	rangeVar->location = forPortionOf->location;
+	result->rangeVar = rangeVar;
+
+	/* Require SELECT privilege on the application-time column. */
+	markVarForSelectPriv(pstate, rangeVar);
+
+	/*
+	 * Use the basetype for the target, which shouldn't be required to follow
+	 * domain rules. The table's column type is in the Var if we need it.
+	 */
+	result->rangeType = attbasetype;
+	result->isDomain = attbasetype != attr->atttypid;
+
+	if (forPortionOf->target)
+	{
+		Oid			declared_target_type = attbasetype;
+		Oid			actual_target_type;
+
+		/*
+		 * We were already given an expression for the target, so we don't
+		 * have to build anything. We still have to make sure we got the right
+		 * type. NULL will be caught be the executor.
+		 */
+
+		result->targetRange = transformExpr(pstate,
+											forPortionOf->target,
+											EXPR_KIND_FOR_PORTION);
+
+		actual_target_type = exprType(result->targetRange);
+
+		if (!can_coerce_type(1, &actual_target_type, &declared_target_type, COERCION_IMPLICIT))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("could not coerce FOR PORTION OF target from %s to %s",
+							format_type_be(actual_target_type),
+							format_type_be(declared_target_type)),
+					 parser_errposition(pstate, exprLocation(forPortionOf->target))));
+
+		result->targetRange = coerce_type(pstate,
+										  result->targetRange,
+										  actual_target_type,
+										  declared_target_type,
+										  -1,
+										  COERCION_IMPLICIT,
+										  COERCE_IMPLICIT_CAST,
+										  exprLocation(forPortionOf->target));
+
+		/*
+		 * XXX: For now we only support ranges and multiranges, so we fail on
+		 * anything else.
+		 */
+		if (!type_is_range(attbasetype) && !type_is_multirange(attbasetype))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("column \"%s\" of relation \"%s\" is not a range or multirange type",
+							range_name,
+							RelationGetRelationName(targetrel)),
+					 parser_errposition(pstate, forPortionOf->location)));
+
+	}
+	else
+	{
+		Oid			rngsubtype;
+		Oid			declared_arg_types[2];
+		Oid			actual_arg_types[2];
+		List	   *args;
+
+		/*
+		 * Make sure it's a range column. XXX: We could support this syntax on
+		 * multirange columns too, if we just built a one-range multirange
+		 * from the FROM/TO phrases.
+		 */
+		if (!type_is_range(attbasetype))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("column \"%s\" of relation \"%s\" is not a range type",
+							range_name,
+							RelationGetRelationName(targetrel)),
+					 parser_errposition(pstate, forPortionOf->location)));
+
+		rngsubtype = get_range_subtype(attbasetype);
+		declared_arg_types[0] = rngsubtype;
+		declared_arg_types[1] = rngsubtype;
+
+		/*
+		 * Build a range from the FROM ... TO ... bounds. This should give a
+		 * constant result, so we accept functions like NOW() but not column
+		 * references, subqueries, etc.
+		 */
+		result->targetFrom = transformExpr(pstate,
+										   forPortionOf->target_start,
+										   EXPR_KIND_FOR_PORTION);
+		result->targetTo = transformExpr(pstate,
+										 forPortionOf->target_end,
+										 EXPR_KIND_FOR_PORTION);
+		actual_arg_types[0] = exprType(result->targetFrom);
+		actual_arg_types[1] = exprType(result->targetTo);
+		args = list_make2(copyObject(result->targetFrom),
+						  copyObject(result->targetTo));
+
+		/*
+		 * Check the bound types separately, for better error message and
+		 * location
+		 */
+		if (!can_coerce_type(1, actual_arg_types, declared_arg_types, COERCION_IMPLICIT))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
+							"FROM",
+							format_type_be(actual_arg_types[0]),
+							format_type_be(declared_arg_types[0])),
+					 parser_errposition(pstate, exprLocation(forPortionOf->target_start))));
+		if (!can_coerce_type(1, &actual_arg_types[1], &declared_arg_types[1], COERCION_IMPLICIT))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("could not coerce FOR PORTION OF %s bound from %s to %s",
+							"TO",
+							format_type_be(actual_arg_types[1]),
+							format_type_be(declared_arg_types[1])),
+					 parser_errposition(pstate, exprLocation(forPortionOf->target_end))));
+
+		make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types);
+		result->targetRange = (Node *) makeFuncExpr(get_range_constructor2(attbasetype),
+													attbasetype,
+													args,
+													InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
+	}
+	if (contain_volatile_functions_after_planning((Expr *) result->targetRange))
+		ereport(ERROR,
+				(errmsg("FOR PORTION OF bounds cannot contain volatile functions")));
+
+	/*
+	 * Build overlapsExpr to use as an extra qual. This means we only hit rows
+	 * matching the FROM & TO bounds. We must look up the overlaps operator
+	 * (usually "&&").
+	 */
+	opclass = GetDefaultOpClass(attr->atttypid, GIST_AM_OID);
+	if (!OidIsValid(opclass))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("data type %s has no default operator class for access method \"%s\"",
+						format_type_be(attr->atttypid), "gist"),
+				 errhint("You must define a default operator class for the data type.")));
+
+	/* Look up the operators and functions we need. */
+	strat = RTOverlapStrategyNumber;
+	GetOperatorFromCompareType(opclass, InvalidOid, COMPARE_OVERLAP, &opid, &strat);
+	op = makeNode(OpExpr);
+	op->opno = opid;
+	op->opfuncid = get_opcode(opid);
+	op->opresulttype = BOOLOID;
+	op->args = list_make2(copyObject(rangeVar), copyObject(result->targetRange));
+	result->overlapsExpr = (Node *) op;
+
+	/*
+	 * Look up the without_portion func. This computes the bounds of temporal
+	 * leftovers.
+	 *
+	 * XXX: Find a more extensible way to look up the function, permitting
+	 * user-defined types. An opclass support function doesn't make sense,
+	 * since there is no index involved. Perhaps a type support function.
+	 */
+	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+		switch (opcintype)
+		{
+			case ANYRANGEOID:
+				result->withoutPortionProc = F_RANGE_MINUS_MULTI;
+				break;
+			case ANYMULTIRANGEOID:
+				result->withoutPortionProc = F_MULTIRANGE_MINUS_MULTI;
+				break;
+			default:
+				elog(ERROR, "unexpected opcintype: %u", opcintype);
+		}
+	else
+		elog(ERROR, "unexpected opclass: %u", opclass);
+
+	if (isUpdate)
+	{
+		/*
+		 * Now make sure we update the start/end time of the record. For a
+		 * range col (r) this is `r = r * targetRange` (where * is the
+		 * intersect operator).
+		 */
+		Oid			intersectoperoid;
+		List	   *funcArgs;
+		Node	   *rangeTLEExpr;
+		TargetEntry *tle;
+
+		/*
+		 * Whatever operator is used for intersect by temporal foreign keys,
+		 * we can use its backing procedure for intersects in FOR PORTION OF.
+		 * XXX: Share code with FindFKPeriodOpers?
+		 */
+		switch (opcintype)
+		{
+			case ANYRANGEOID:
+				intersectoperoid = OID_RANGE_INTERSECT_RANGE_OP;
+				break;
+			case ANYMULTIRANGEOID:
+				intersectoperoid = OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP;
+				break;
+			default:
+				elog(ERROR, "unexpected opcintype: %u", opcintype);
+		}
+		funcid = get_opcode(intersectoperoid);
+		if (!OidIsValid(funcid))
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("could not identify an intersect function for type %s",
+						   format_type_be(opcintype)));
+
+		funcArgs = list_make2(copyObject(rangeVar),
+							  copyObject(result->targetRange));
+		rangeTLEExpr = (Node *) makeFuncExpr(funcid, attbasetype, funcArgs,
+											 InvalidOid, InvalidOid,
+											 COERCE_EXPLICIT_CALL);
+
+		/*
+		 * Coerce to domain if necessary. If we skip this, we will allow
+		 * updating to forbidden values.
+		 */
+		rangeTLEExpr = coerce_type(pstate,
+								   rangeTLEExpr,
+								   attbasetype,
+								   attr->atttypid,
+								   -1,
+								   COERCION_IMPLICIT,
+								   COERCE_IMPLICIT_CAST,
+								   exprLocation(forPortionOf->target));
+
+		/* Make a TLE to set the range column */
+		result->rangeTargetList = NIL;
+		tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno, range_name,
+							  false);
+		result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+		/*
+		 * The range column will change, but you don't need UPDATE permission
+		 * on it, so we don't add to updatedCols here.
+		 * XXX: If
+		 * https://www.postgresql.org/message-id/CACJufxEtY1hdLcx%3DFhnqp-ERcV1PhbvELG5COy_CZjoEW76ZPQ%40mail.gmail.com
+		 * is merged (only validate CHECK constraints if they depend on one of
+		 * the columns being UPDATEd), we need to make sure that code knows that
+		 * we are updating the application-time column.
+		 */
+	}
+	else
+		result->rangeTargetList = NIL;
+
+	result->range_name = range_name;
+
+	return result;
+}
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2518,6 +2849,13 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 										 stmt->relation->inh,
 										 true,
 										 ACL_UPDATE);
+
+	if (stmt->forPortionOf)
+		qry->forPortionOf = transformForPortionOfClause(pstate,
+														qry->resultRelation,
+														stmt->forPortionOf,
+														true);
+
 	nsitem = pstate->p_target_nsitem;
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2544,7 +2882,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	 * Now we are done with SELECT-like processing, and can get on with
 	 * transforming the target list to match the UPDATE target columns.
 	 */
-	qry->targetList = transformUpdateTargetList(pstate, stmt->targetList);
+	qry->targetList = transformUpdateTargetList(pstate, stmt->targetList,
+												qry->forPortionOf);
 
 	qry->rtable = pstate->p_rtable;
 	qry->rteperminfos = pstate->p_rteperminfos;
@@ -2563,7 +2902,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
  *	handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE
  */
 List *
-transformUpdateTargetList(ParseState *pstate, List *origTlist)
+transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf)
 {
 	List	   *tlist = NIL;
 	RTEPermissionInfo *target_perminfo;
@@ -2616,6 +2955,20 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 					 errhint("SET target columns cannot be qualified with the relation name.") : 0,
 					 parser_errposition(pstate, origTarget->location)));
 
+		/*
+		 * If this is a FOR PORTION OF update, forbid directly setting the
+		 * range column, since that would conflict with the implicit updates.
+		 */
+		if (forPortionOf != NULL)
+		{
+			if (attrno == forPortionOf->rangeVar->varattno)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("cannot update column \"%s\" because it is used in FOR PORTION OF",
+								origTarget->name),
+						 parser_errposition(pstate, origTarget->location)));
+		}
+
 		updateTargetListEntry(pstate, tle, origTarget->name,
 							  attrno,
 							  origTarget->indirection,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c567252acc4..778959c5bbb 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -556,6 +556,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <range>	relation_expr
 %type <range>	extended_relation_expr
 %type <range>	relation_expr_opt_alias
+%type <alias>	opt_alias
+%type <node>	for_portion_of_clause
 %type <node>	tablesample_clause opt_repeatable_clause
 %type <target>	target_el set_target insert_column_item
 
@@ -766,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
-	PERIOD PLACING PLAN PLANS POLICY
+	PERIOD PLACING PLAN PLANS POLICY PORTION
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -885,12 +887,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * json_predicate_type_constraint and json_key_uniqueness_constraint_opt
  * productions (see comments there).
  *
+ * TO is assigned the same precedence as IDENT, to support the opt_interval
+ * production (see comment there).
+ *
  * Like the UNBOUNDED PRECEDING/FOLLOWING case, NESTED is assigned a lower
  * precedence than PATH to fix ambiguity in the json_table production.
  */
 %nonassoc	UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
 %nonassoc	IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
-			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
+			SET KEYS OBJECT_P SCALAR TO USING VALUE_P WITH WITHOUT PATH
 %left		Op OPERATOR		/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -12620,6 +12625,20 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
+			| opt_with_clause DELETE_P FROM relation_expr for_portion_of_clause opt_alias
+			using_clause where_or_current_clause returning_clause
+				{
+					DeleteStmt *n = makeNode(DeleteStmt);
+
+					n->relation = $4;
+					n->forPortionOf = (ForPortionOfClause *) $5;
+					n->relation->alias = $6;
+					n->usingClause = $7;
+					n->whereClause = $8;
+					n->returningClause = $9;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 using_clause:
@@ -12694,6 +12713,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
+			| opt_with_clause UPDATE relation_expr
+			for_portion_of_clause opt_alias
+			SET set_clause_list
+			from_clause
+			where_or_current_clause
+			returning_clause
+				{
+					UpdateStmt *n = makeNode(UpdateStmt);
+
+					n->relation = $3;
+					n->forPortionOf = (ForPortionOfClause *) $4;
+					n->relation->alias = $5;
+					n->targetList = $7;
+					n->fromClause = $8;
+					n->whereClause = $9;
+					n->returningClause = $10;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 set_clause_list:
@@ -14196,6 +14234,44 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+opt_alias:
+			AS ColId
+				{
+					Alias	   *alias = makeNode(Alias);
+
+					alias->aliasname = $2;
+					$$ = alias;
+				}
+			| BareColLabel
+				{
+					Alias	   *alias = makeNode(Alias);
+
+					alias->aliasname = $1;
+					$$ = alias;
+				}
+			| /* empty */ %prec UMINUS { $$ = NULL; }
+		;
+
+for_portion_of_clause:
+			FOR PORTION OF ColId '(' a_expr ')'
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->location = @4;
+					n->target = $6;
+					$$ = (Node *) n;
+				}
+			| FOR PORTION OF ColId FROM a_expr TO a_expr
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->location = @4;
+					n->target_start = $6;
+					n->target_end = $8;
+					$$ = (Node *) n;
+				}
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -15036,16 +15112,25 @@ opt_timezone:
 			| /*EMPTY*/								{ $$ = false; }
 		;
 
+/*
+ * We need to handle this shift/reduce conflict:
+ * FOR PORTION OF valid_at FROM t + INTERVAL '1' YEAR TO MONTH.
+ * We don't see far enough ahead to know if there is another TO coming.
+ * We prefer to interpret this as FROM (t + INTERVAL '1' YEAR TO MONTH),
+ * i.e. to shift.
+ * That gives the user the option of adding parentheses to get the other meaning.
+ * If we reduced, intervals could never have a TO.
+ */
 opt_interval:
-			YEAR_P
+			YEAR_P																%prec IS
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(YEAR), @1)); }
 			| MONTH_P
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(MONTH), @1)); }
-			| DAY_P
+			| DAY_P																%prec IS
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY), @1)); }
-			| HOUR_P
+			| HOUR_P															%prec IS
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(HOUR), @1)); }
-			| MINUTE_P
+			| MINUTE_P														%prec IS
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); }
 			| interval_second
 				{ $$ = $1; }
@@ -18121,6 +18206,7 @@ unreserved_keyword:
 			| PLAN
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -18754,6 +18840,7 @@ bare_label_keyword:
 			| PLAN
 			| PLANS
 			| POLICY
+			| PORTION
 			| POSITION
 			| PRECEDING
 			| PREPARE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 25ee0f87d93..e3e5a5c9cce 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -583,6 +583,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			if (isAgg)
+				err = _("aggregate functions are not allowed in FOR PORTION OF expressions");
+			else
+				err = _("grouping operations are not allowed in FOR PORTION OF expressions");
+
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1023,6 +1030,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("window functions are not allowed in FOR PORTION OF expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index ba7df2a7789..2f2da1f4203 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -484,6 +484,7 @@ assign_collations_walker(Node *node, assign_collations_context *context)
 		case T_JoinExpr:
 		case T_FromExpr:
 		case T_OnConflictExpr:
+		case T_ForPortionOfExpr:
 		case T_SortGroupClause:
 		case T_MergeAction:
 			(void) expression_tree_walker(node,
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index dcfe1acc4c3..49a7dafc2b4 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -586,6 +586,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_PARTITION_BOUND:
 			err = _("cannot use column reference in partition bound expression");
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("cannot use column reference in FOR PORTION OF expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1871,6 +1874,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_GENERATED_COLUMN:
 			err = _("cannot use subquery in column generation expression");
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("cannot use subquery in FOR PORTION OF expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3230,6 +3236,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "GENERATED AS";
 		case EXPR_KIND_CYCLE_MARK:
 			return "CYCLE";
+		case EXPR_KIND_FOR_PORTION:
+			return "FOR PORTION OF";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 24f6745923b..1096aa1769e 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 0a70d48fd4c..2e6dd166c98 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -381,7 +381,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 			case CMD_UPDATE:
 				action->targetList =
 					transformUpdateTargetList(pstate,
-											  mergeWhenClause->targetList);
+											  mergeWhenClause->targetList, NULL);
 				break;
 			case CMD_DELETE:
 				break;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 7c99290be4d..4c96a84b048 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3745,6 +3745,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 									  &parsetree->hasSubLinks);
 	}
 
+	if (parsetree->forPortionOf && parsetree->commandType == CMD_UPDATE)
+	{
+		/*
+		 * Like the INSERT/UPDATE code above, update the resnos in the
+		 * auxiliary UPDATE targetlist to refer to columns of the base
+		 * relation.
+		 */
+		foreach(lc, parsetree->forPortionOf->rangeTargetList)
+		{
+			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+			TargetEntry *view_tle;
+
+			if (tle->resjunk)
+				continue;
+
+			view_tle = get_tle_by_resno(view_targetlist, tle->resno);
+			if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
+				tle->resno = ((Var *) view_tle->expr)->varattno;
+			else
+				elog(ERROR, "attribute number %d not found in view targetlist",
+					 tle->resno);
+		}
+	}
+
 	/*
 	 * For UPDATE/DELETE/MERGE, pull up any WHERE quals from the view.  We
 	 * know that any Vars in the quals must reference the one base relation,
@@ -4101,6 +4125,37 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 		else if (event == CMD_UPDATE)
 		{
 			Assert(parsetree->override == OVERRIDING_NOT_SET);
+
+			if (parsetree->forPortionOf)
+			{
+				/*
+				 * Don't add FOR PORTION OF details until we're done rewriting
+				 * a view update, so that we don't add the same qual and TLE
+				 * on the recursion.
+				 *
+				 * Views don't need to do anything special here to remap Vars;
+				 * that is handled by the tree walker.
+				 */
+				if (rt_entry_relation->rd_rel->relkind != RELKIND_VIEW)
+				{
+					ListCell   *tl;
+
+					/*
+					 * Add qual: UPDATE FOR PORTION OF should be limited to
+					 * rows that overlap the target range.
+					 */
+					AddQual(parsetree, parsetree->forPortionOf->overlapsExpr);
+
+					/* Update FOR PORTION OF column(s) automatically. */
+					foreach(tl, parsetree->forPortionOf->rangeTargetList)
+					{
+						TargetEntry *tle = (TargetEntry *) lfirst(tl);
+
+						parsetree->targetList = lappend(parsetree->targetList, tle);
+					}
+				}
+			}
+
 			parsetree->targetList =
 				rewriteTargetListIU(parsetree->targetList,
 									parsetree->commandType,
@@ -4146,7 +4201,25 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 		}
 		else if (event == CMD_DELETE)
 		{
-			/* Nothing to do here */
+			if (parsetree->forPortionOf)
+			{
+				/*
+				 * Don't add FOR PORTION OF details until we're done rewriting
+				 * a view delete, so that we don't add the same qual on the
+				 * recursion.
+				 *
+				 * Views don't need to do anything special here to remap Vars;
+				 * that is handled by the tree walker.
+				 */
+				if (rt_entry_relation->rd_rel->relkind != RELKIND_VIEW)
+				{
+					/*
+					 * Add qual: DELETE FOR PORTION OF should be limited to
+					 * rows that overlap the target range.
+					 */
+					AddQual(parsetree, parsetree->forPortionOf->overlapsExpr);
+				}
+			}
 		}
 		else
 			elog(ERROR, "unrecognized commandType: %d", (int) event);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 89cbdd3b1e7..f1c15d00d52 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -516,6 +516,8 @@ static void get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
 						  deparse_context *context);
 static void get_column_alias_list(deparse_columns *colinfo,
 								  deparse_context *context);
+static void get_for_portion_of(ForPortionOfExpr *forPortionOf,
+							   deparse_context *context);
 static void get_from_clause_coldeflist(RangeTblFunction *rtfunc,
 									   deparse_columns *colinfo,
 									   deparse_context *context);
@@ -7194,6 +7196,9 @@ get_update_query_def(Query *query, deparse_context *context)
 					 only_marker(rte),
 					 generate_relation_name(rte->relid, NIL));
 
+	/* Print the FOR PORTION OF, if needed */
+	get_for_portion_of(query->forPortionOf, context);
+
 	/* Print the relation alias, if needed */
 	get_rte_alias(rte, query->resultRelation, false, context);
 
@@ -7398,6 +7403,9 @@ get_delete_query_def(Query *query, deparse_context *context)
 					 only_marker(rte),
 					 generate_relation_name(rte->relid, NIL));
 
+	/* Print the FOR PORTION OF, if needed */
+	get_for_portion_of(query->forPortionOf, context);
+
 	/* Print the relation alias, if needed */
 	get_rte_alias(rte, query->resultRelation, false, context);
 
@@ -12767,6 +12775,39 @@ get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
 						 quote_identifier(refname));
 }
 
+/*
+ * get_for_portion_of - print FOR PORTION OF if needed
+ * XXX: Newlines would help here, at least when pretty-printing. But then the
+ * alias and SET will be on their own line with a leading space.
+ */
+static void
+get_for_portion_of(ForPortionOfExpr *forPortionOf, deparse_context *context)
+{
+	if (forPortionOf)
+	{
+		appendStringInfo(context->buf, " FOR PORTION OF %s",
+						 quote_identifier(forPortionOf->range_name));
+
+		/*
+		 * Try to write it as FROM ... TO ... if we received it that way,
+		 * otherwise (targetExpr).
+		 */
+		if (forPortionOf->targetFrom && forPortionOf->targetTo)
+		{
+			appendStringInfoString(context->buf, " FROM ");
+			get_rule_expr(forPortionOf->targetFrom, context, false);
+			appendStringInfoString(context->buf, " TO ");
+			get_rule_expr(forPortionOf->targetTo, context, false);
+		}
+		else
+		{
+			appendStringInfoString(context->buf, " (");
+			get_rule_expr(forPortionOf->targetRange, context, false);
+			appendStringInfoString(context->buf, ")");
+		}
+	}
+}
+
 /*
  * get_column_alias_list - print column alias list for an RTE
  *
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 63c067d5aae..637b02c3644 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -50,6 +50,7 @@
 #include "utils/sortsupport.h"
 #include "utils/tuplesort.h"
 #include "utils/tuplestore.h"
+#include "utils/typcache.h"
 
 /*
  * forward references in this file
@@ -455,6 +456,24 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
+ * Executor state of a FOR PORTION OF operation.
+ */
+typedef struct ForPortionOfState
+{
+	NodeTag		type;
+
+	char	   *fp_rangeName;	/* the column named in FOR PORTION OF */
+	Oid			fp_rangeType;	/* the type of the FOR PORTION OF expression */
+	int			fp_rangeAttno;	/* the attno of the range column */
+	Datum		fp_targetRange; /* the range/multirange from FOR PORTION OF */
+	TypeCacheEntry *fp_leftoverstypcache;	/* type cache entry of the range */
+	TupleTableSlot *fp_Existing;	/* slot to store old tuple */
+	TupleTableSlot *fp_Leftover;	/* slot to store leftover */
+} ForPortionOfState;
+
 /*
  * ResultRelInfo
  *
@@ -591,6 +610,9 @@ typedef struct ResultRelInfo
 	/* for MERGE, expr state for checking the join condition */
 	ExprState  *ri_MergeJoinCondition;
 
+	/* FOR PORTION OF evaluation state */
+	ForPortionOfState *ri_forPortionOf;
+
 	/* partition check expression state (NULL if not set up yet) */
 	ExprState  *ri_PartitionCheckExpr;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0aec49bdd22..1396606b1fa 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -147,6 +147,9 @@ typedef struct Query
 	 */
 	int			resultRelation pg_node_attr(query_jumble_ignore);
 
+	/* FOR PORTION OF clause for UPDATE/DELETE */
+	ForPortionOfExpr *forPortionOf;
+
 	/* has aggregates in tlist or havingQual */
 	bool		hasAggs pg_node_attr(query_jumble_ignore);
 	/* has window functions in tlist */
@@ -1641,6 +1644,21 @@ typedef struct RowMarkClause
 	bool		pushedDown;		/* pushed down from higher query level? */
 } RowMarkClause;
 
+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <range-name> FROM <target-start> TO
+ *		<target-end> or FOR PORTION OF <range-name> (<target>)
+ */
+typedef struct ForPortionOfClause
+{
+	NodeTag		type;
+	char	   *range_name;
+	ParseLoc	location;
+	Node	   *target;
+	Node	   *target_start;
+	Node	   *target_end;
+} ForPortionOfClause;
+
 /*
  * WithClause -
  *	   representation of WITH clause
@@ -2155,6 +2173,7 @@ typedef struct DeleteStmt
 	Node	   *whereClause;	/* qualifications */
 	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
 } DeleteStmt;
 
 /* ----------------------
@@ -2170,6 +2189,7 @@ typedef struct UpdateStmt
 	List	   *fromClause;		/* optional from clause for more tables */
 	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
 } UpdateStmt;
 
 /* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c175ee95b68..77ab8e972ab 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2707,6 +2707,7 @@ typedef struct ModifyTablePath
 	List	   *returningLists; /* per-target-table RETURNING tlists */
 	List	   *rowMarks;		/* PlanRowMarks (non-locking only) */
 	OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */
+	ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
 	int			epqParam;		/* ID of Param for EvalPlanQual re-eval */
 	List	   *mergeActionLists;	/* per-target-table lists of actions for
 									 * MERGE */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 8c9321aab8c..3c980ee18bb 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -376,6 +376,8 @@ typedef struct ModifyTable
 	List	   *onConflictCols;
 	/* WHERE for ON CONFLICT DO SELECT/UPDATE */
 	Node	   *onConflictWhere;
+	/* FOR PORTION OF clause for UPDATE/DELETE */
+	Node	   *forPortionOf;
 	/* RTI of the EXCLUDED pseudo relation */
 	Index		exclRelRTI;
 	/* tlist of the EXCLUDED pseudo relation */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 384df50c80a..524b5c73e39 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2391,4 +2391,37 @@ typedef struct OnConflictExpr
 	List	   *exclRelTlist;	/* tlist of the EXCLUDED pseudo relation */
 } OnConflictExpr;
 
+/*----------
+ * ForPortionOfExpr - represents a FOR PORTION OF ... expression
+ *
+ * We set up an expression to make a range from the FROM/TO bounds,
+ * so that we can use range operators with it.
+ *
+ * Then we set up an overlaps expression between that and the range column,
+ * so that we can find the rows we need to update/delete.
+ *
+ * If the user used the FROM ... TO ... syntax, we save the individual
+ * expressions so that we can deparse them.
+ *
+ * In the executor we'll also build an intersect expression between the
+ * targeted range and the range column, so that we can update the start/end
+ * bounds of the UPDATE'd record.
+ *----------
+ */
+typedef struct ForPortionOfExpr
+{
+	NodeTag		type;
+	Var		   *rangeVar;		/* Range column */
+	char	   *range_name;		/* Range name */
+	Node	   *targetFrom;		/* FOR PORTION OF FROM bound, if given */
+	Node	   *targetTo;		/* FOR PORTION OF TO bound, if given */
+	Node	   *targetRange;	/* FOR PORTION OF bounds as a range/multirange */
+	Oid			rangeType;		/* (base)type of targetRange */
+	bool		isDomain;		/* Is rangeVar a domain? */
+	Node	   *overlapsExpr;	/* range && targetRange */
+	List	   *rangeTargetList;	/* List of TargetEntrys to set the time
+									 * column(s) */
+	Oid			withoutPortionProc; /* SRF proc for old_range - target_range */
+} ForPortionOfExpr;
+
 #endif							/* PRIMNODES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index cf8a654fa53..5db7858876e 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -313,7 +313,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
 												List *mergeActionLists, List *mergeJoinConditions,
-												int epqParam);
+												ForPortionOfExpr *forPortionOf, int epqParam);
 extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
 									Path *subpath,
 									Node *limitOffset, Node *limitCount,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index abc5f11cafd..090121c7505 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -43,7 +43,8 @@ extern List *transformInsertRow(ParseState *pstate, List *exprlist,
 								List *stmtcols, List *icolumns, List *attrnos,
 								bool strip_indirection);
 extern List *transformUpdateTargetList(ParseState *pstate,
-									   List *origTlist);
+									   List *origTlist,
+									   ForPortionOfExpr *forPortionOf);
 extern void transformReturningClause(ParseState *pstate, Query *qry,
 									 ReturningClause *returningClause,
 									 ParseExprKind exprKind);
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7753c5c8a8..c1c92de88e8 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -348,6 +348,7 @@ PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f23e21f318b..3eaeb7a90e1 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -56,6 +56,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_UPDATE_SOURCE,	/* UPDATE assignment source item */
 	EXPR_KIND_UPDATE_TARGET,	/* UPDATE assignment target item */
 	EXPR_KIND_MERGE_WHEN,		/* MERGE WHEN [NOT] MATCHED condition */
+	EXPR_KIND_FOR_PORTION,		/* UPDATE/DELETE FOR PORTION OF item */
 	EXPR_KIND_GROUP_BY,			/* GROUP BY */
 	EXPR_KIND_ORDER_BY,			/* ORDER BY */
 	EXPR_KIND_DISTINCT_ON,		/* DISTINCT ON */
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
new file mode 100644
index 00000000000..24caed16691
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,2067 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+SET datestyle TO ISO, YMD;
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = 'one^1';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-01-15) | one
+ [1,2) | [2018-01-15,2019-01-01) | one^1
+ [1,2) | [2019-01-01,2020-01-01) | one
+(3 rows)
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-01-15) | one
+ [1,2) | [2018-01-15,2019-01-01) | one^1
+ [1,2) | [2019-01-01,2019-01-15) | one
+ [1,2) | [2019-01-20,2020-01-01) | one
+(4 rows)
+
+-- With a table alias with AS
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t
+  SET name = 'one^2';
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t;
+-- With a table alias without AS
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t
+  SET name = 'one^3';
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t;
+-- UPDATE with FROM
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02'
+  SET name = 'one^4'
+  FROM (SELECT '[1,2)'::int4range) AS t2(id)
+  WHERE for_portion_of_test.id = t2.id;
+-- DELETE with USING
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03'
+  USING (SELECT '[1,2)'::int4range) AS t2(id)
+  WHERE for_portion_of_test.id = t2.id;
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-01-15) | one
+ [1,2) | [2018-01-15,2019-01-01) | one^1
+ [1,2) | [2019-01-01,2019-01-15) | one
+ [1,2) | [2019-01-20,2019-02-01) | one
+ [1,2) | [2019-02-01,2019-02-03) | one^2
+ [1,2) | [2019-02-04,2019-02-05) | one^3
+ [1,2) | [2019-02-06,2019-03-01) | one
+ [1,2) | [2019-03-01,2019-03-02) | one^4
+ [1,2) | [2019-03-03,2020-01-01) | one
+(9 rows)
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid1_at daterange,
+  valid2_at daterange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL
+  SET name = 'foo';
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+  id   |        valid1_at        |        valid2_at        | name 
+-------+-------------------------+-------------------------+------
+ [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2025-01-01) | one
+ [1,2) | [2018-01-15,2018-02-03) | [2015-01-01,2025-01-01) | foo
+(2 rows)
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL
+  SET name = 'bar';
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+  id   |        valid1_at        |        valid2_at        | name 
+-------+-------------------------+-------------------------+------
+ [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one
+ [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2025-01-01) | bar
+ [1,2) | [2018-01-15,2018-02-03) | [2015-01-01,2018-01-15) | foo
+ [1,2) | [2018-01-15,2018-02-03) | [2018-01-15,2025-01-01) | bar
+(4 rows)
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+  id   |        valid1_at        |        valid2_at        | name 
+-------+-------------------------+-------------------------+------
+ [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one
+ [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2025-01-01) | bar
+ [1,2) | [2018-01-15,2018-01-20) | [2015-01-01,2018-01-15) | foo
+ [1,2) | [2018-01-15,2018-01-20) | [2018-01-15,2025-01-01) | bar
+(4 rows)
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+  id   |        valid1_at        |        valid2_at        | name 
+-------+-------------------------+-------------------------+------
+ [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one
+ [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2018-01-20) | bar
+ [1,2) | [2018-01-15,2018-01-20) | [2015-01-01,2018-01-15) | foo
+ [1,2) | [2018-01-15,2018-01-20) | [2018-01-15,2018-01-20) | bar
+(4 rows)
+
+-- Test with NULLs in the scalar/range key columns.
+-- This won't happen if there is a PRIMARY KEY or UNIQUE constraint
+-- but FOR PORTION OF shouldn't require that.
+DROP TABLE for_portion_of_test;
+CREATE UNLOGGED TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', NULL, '1 null'),
+  ('[1,2)', '(,)', '1 unbounded'),
+  ('[1,2)', 'empty', '1 empty'),
+  (NULL, NULL, NULL),
+  (NULL, daterange('2018-01-01', '2019-01-01'), 'null key');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'NULL to NULL';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         |     name     
+-------+-------------------------+--------------
+ [1,2) | empty                   | 1 empty
+ [1,2) | (,)                     | NULL to NULL
+ [1,2) |                         | 1 null
+       | [2018-01-01,2019-01-01) | NULL to NULL
+       |                         | 
+(5 rows)
+
+DROP TABLE for_portion_of_test;
+--
+-- UPDATE tests
+--
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at daterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three'),
+  ('[4,5)', '(,2018-04-01)', 'four'),
+  ('[5,6)', '(,)', 'five')
+  ;
+\set QUIET false
+-- Updating with a missing column fails
+UPDATE for_portion_of_test
+  FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+  SET name = 'foo'
+  WHERE id = '[5,6)';
+ERROR:  column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2:   FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+                         ^
+-- Updating the range fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  SET valid_at = '[1990-01-01,1999-01-01)'
+  WHERE id = '[5,6)';
+ERROR:  cannot update column "valid_at" because it is used in FOR PORTION OF
+LINE 3:   SET valid_at = '[1990-01-01,1999-01-01)'
+              ^
+-- The wrong start type fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+ERROR:  could not coerce FOR PORTION OF FROM bound from integer to date
+LINE 2:   FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+                                       ^
+-- The wrong end type fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+ERROR:  could not coerce FOR PORTION OF TO bound from integer to date
+LINE 2:   FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+                                                       ^
+-- Updating with timestamps reversed fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+ERROR:  range lower bound must be less than or equal to range upper bound
+-- Updating with a subquery fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+ERROR:  cannot use subquery in FOR PORTION OF expression
+LINE 2:   FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '201...
+                                       ^
+-- Updating with a column fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+ERROR:  cannot use column reference in FOR PORTION OF expression
+LINE 2:   FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+                                             ^
+-- Updating with timestamps equal does nothing
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+  SET name = 'three^0'
+  WHERE id = '[3,4)';
+UPDATE 0
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [3,4) | [2018-01-01,2018-06-01) | three
+ [3,4) | [2018-06-01,)           | three^1
+(2 rows)
+
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+  SET name = 'three^2'
+  WHERE id = '[3,4)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [3,4) | [2018-01-01,2018-03-01) | three^2
+ [3,4) | [2018-03-01,2018-06-01) | three
+ [3,4) | [2018-06-01,)           | three^1
+(3 rows)
+
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+  SET name = 'four^1'
+  WHERE id = '[4,5)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [4,5) | (,2018-02-01)           | four^1
+ [4,5) | [2018-02-01,2018-04-01) | four
+(2 rows)
+
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
+  SET name = 'four^2'
+  WHERE id = '[4,5)';
+UPDATE 2
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [4,5) | (,2017-01-01)           | four^1
+ [4,5) | [2017-01-01,2018-02-01) | four^2
+ [4,5) | [2018-02-01,2018-04-01) | four^2
+(3 rows)
+
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+  SET name = 'four^3'
+  WHERE id = '[4,5)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [4,5) | (,2017-01-01)           | four^1
+ [4,5) | [2017-01-01,2018-02-01) | four^3
+ [4,5) | [2018-02-01,2018-04-01) | four^2
+(3 rows)
+
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'two^2'
+  WHERE id = '[2,3)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [2,3) | [2018-01-01,2018-01-05) | two^2
+(1 row)
+
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+  SET name = 'five^1'
+  WHERE id = '[5,6)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [5,6) | (,2018-01-01)           | five
+ [5,6) | [2018-01-01,2019-01-01) | five^1
+ [5,6) | [2019-01-01,)           | five
+(3 rows)
+
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+  SET name = 'five^2'
+  WHERE id = '[5,6)';
+UPDATE 3
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [5,6) | (,2017-01-01)           | five
+ [5,6) | [2017-01-01,2018-01-01) | five^2
+ [5,6) | [2018-01-01,2019-01-01) | five^2
+ [5,6) | [2019-01-01,2020-01-01) | five^2
+ [5,6) | [2020-01-01,)           | five
+(5 rows)
+
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+UPDATE 3
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-03-03) | one^2
+ [1,2) | [2018-03-03,2018-04-04) | one^2
+(3 rows)
+
+-- Updating with a direct target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15'))
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-03-03) | one^2
+ [1,2) | [2018-03-03,2018-03-10) | one^2
+ [1,2) | [2018-03-10,2018-03-15) | one^3
+ [1,2) | [2018-03-15,2018-04-04) | one^2
+(5 rows)
+
+-- Updating with a direct target, coerced from a string
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+UPDATE 1
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-03-03) | one^2
+ [1,2) | [2018-03-03,2018-03-10) | one^2
+ [1,2) | [2018-03-10,2018-03-15) | one^3
+ [1,2) | [2018-03-15,2018-03-17) | one^3
+ [1,2) | [2018-03-17,2018-04-04) | one^2
+(6 rows)
+
+-- Updating with a direct target of the wrong range subtype fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (int4range(1, 4))
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from int4range to daterange
+LINE 2:   FOR PORTION OF valid_at (int4range(1, 4))
+                                   ^
+-- Updating with a direct target of a non-rangetype fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (4)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from integer to daterange
+LINE 2:   FOR PORTION OF valid_at (4)
+                                   ^
+-- Updating with a direct target of NULL fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (NULL)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  got a NULL FOR PORTION OF target
+-- Updating with a direct target of empty does nothing
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at ('empty')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+UPDATE 0
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-03-03) | one^2
+ [1,2) | [2018-03-03,2018-03-10) | one^2
+ [1,2) | [2018-03-10,2018-03-15) | one^3
+ [1,2) | [2018-03-15,2018-03-17) | one^3
+ [1,2) | [2018-03-17,2018-04-04) | one^2
+(6 rows)
+
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+  SET id = '[6,7)'
+  WHERE id = '[1,2)';
+UPDATE 5
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-02-15) | one^2
+(2 rows)
+
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+  SET name = name || '*';
+UPDATE 2
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         |   name   
+-------+-------------------------+----------
+ [1,2) | [2018-01-02,2018-02-03) | one^2
+ [1,2) | [2018-02-03,2018-02-15) | one^2
+ [2,3) | [2018-01-01,2018-01-05) | two^2
+ [3,4) | [2018-01-01,2018-03-01) | three^2
+ [3,4) | [2018-03-01,2018-06-01) | three
+ [3,4) | [2018-06-01,2030-01-01) | three^1
+ [3,4) | [2030-01-01,)           | three^1*
+ [4,5) | (,2017-01-01)           | four^1
+ [4,5) | [2017-01-01,2018-02-01) | four^3
+ [4,5) | [2018-02-01,2018-04-01) | four^2
+ [5,6) | (,2017-01-01)           | five
+ [5,6) | [2017-01-01,2018-01-01) | five^2
+ [5,6) | [2018-01-01,2019-01-01) | five^2
+ [5,6) | [2019-01-01,2020-01-01) | five^2
+ [5,6) | [2020-01-01,2030-01-01) | five
+ [5,6) | [2030-01-01,)           | five*
+ [6,7) | [2018-02-15,2018-03-03) | one^2
+ [6,7) | [2018-03-03,2018-03-10) | one^2
+ [6,7) | [2018-03-10,2018-03-15) | one^3
+ [6,7) | [2018-03-15,2018-03-17) | one^3
+ [6,7) | [2018-03-17,2018-04-04) | one^2
+(21 rows)
+
+\set QUIET true
+-- Updating with a shift/reduce conflict
+-- (requires a tsrange column)
+CREATE UNLOGGED TABLE for_portion_of_test2 (
+  id int4range,
+  valid_at tsrange,
+  name text
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', '[2000-01-01,2020-01-01)', 'one');
+-- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes)
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE
+    TO '2012-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+-- TO is used for the bound but not the INTERVAL:
+-- syntax error
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR
+    TO '2014-01-01'
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+ERROR:  syntax error at or near "'2014-01-01'"
+LINE 4:     TO '2014-01-01'
+               ^
+-- adding parens fixes it
+-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes)
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR)
+    TO '2016-01-01'
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+  id   |                   valid_at                    | name  
+-------+-----------------------------------------------+-------
+ [1,2) | ["2000-01-01 00:00:00","2011-03-01 01:02:00") | one
+ [1,2) | ["2011-03-01 01:02:00","2012-01-01 00:00:00") | one^1
+ [1,2) | ["2012-01-01 00:00:00","2015-03-01 01:00:00") | one
+ [1,2) | ["2015-03-01 01:00:00","2016-01-01 00:00:00") | one^3
+ [1,2) | ["2016-01-01 00:00:00","2020-01-01 00:00:00") | one
+(5 rows)
+
+DROP TABLE for_portion_of_test2;
+-- UPDATE FOR PORTION OF in a CTE:
+-- Visible to SELECT:
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+WITH update_apr AS (
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01'
+    SET name = 'Apr 2018'
+    WHERE id = '[10,11)'
+    RETURNING id, valid_at, name
+)
+SELECT *
+  FROM for_portion_of_test AS t, update_apr
+  WHERE t.id = update_apr.id;
+   id    |        valid_at         | name |   id    |        valid_at         |   name   
+---------+-------------------------+------+---------+-------------------------+----------
+ [10,11) | [2018-01-01,2020-01-01) | ten  | [10,11) | [2018-04-01,2018-05-01) | Apr 2018
+(1 row)
+
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+   id    |        valid_at         |   name   
+---------+-------------------------+----------
+ [10,11) | [2018-04-01,2018-05-01) | Apr 2018
+ [10,11) | [2018-01-01,2018-04-01) | ten
+ [10,11) | [2018-05-01,2020-01-01) | ten
+(3 rows)
+
+-- UPDATE FOR PORTION OF with current_date
+-- (We take care not to make the expectation depend on the timestamp.)
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[99,100)', '[2000-01-01,)', 'foo');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM current_date TO null
+  SET name = 'bar'
+  WHERE id = '[99,100)';
+SELECT name, lower(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date - 1;
+ name |   lower    
+------+------------
+ foo  | 2000-01-01
+(1 row)
+
+SELECT name, upper(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date + 1;
+ name | upper 
+------+-------
+ bar  | 
+(1 row)
+
+-- UPDATE FOR PORTION OF with clock_timestamp()
+-- fails because the function is volatile:
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM clock_timestamp()::date TO null
+  SET name = 'baz'
+  WHERE id = '[99,100)';
+ERROR:  FOR PORTION OF bounds cannot contain volatile functions
+-- clean up:
+DELETE FROM for_portion_of_test WHERE id = '[99,100)';
+-- Not visible to UPDATE:
+-- Tuples updated/inserted within the CTE are not visible to the main query yet,
+-- but neither are old tuples the CTE changed:
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven');
+WITH update_apr AS (
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01'
+    SET name = 'Apr 2018'
+    WHERE id = '[11,12)'
+    RETURNING id, valid_at, name
+)
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  AS t
+  SET name = 'May 2018'
+  FROM update_apr AS j
+  WHERE t.id = j.id;
+SELECT * FROM for_portion_of_test WHERE id = '[11,12)';
+   id    |        valid_at         |   name   
+---------+-------------------------+----------
+ [11,12) | [2018-04-01,2018-05-01) | Apr 2018
+ [11,12) | [2018-01-01,2018-04-01) | eleven
+ [11,12) | [2018-05-01,2020-01-01) | eleven
+(3 rows)
+
+DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)');
+-- UPDATE FOR PORTION OF in a PL/pgSQL function
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date)
+RETURNS void LANGUAGE plpgsql AS
+$$
+BEGIN
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM $2 TO $3
+    SET name = concat(_target_from::text, ' to ', _target_til::text)
+    WHERE id = $1;
+END;
+$$;
+SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01');
+ fpo_update 
+------------
+ 
+(1 row)
+
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+   id    |        valid_at         |           name           
+---------+-------------------------+--------------------------
+ [10,11) | [2018-01-01,2019-01-01) | 2015-01-01 to 2019-01-01
+ [10,11) | [2019-01-01,2020-01-01) | ten
+(2 rows)
+
+-- UPDATE FOR PORTION OF in a compiled SQL function
+CREATE FUNCTION fpo_update()
+RETURNS text
+BEGIN ATOMIC
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+    SET name = 'one^1'
+    RETURNING name;
+END;
+\sf+ fpo_update()
+        CREATE OR REPLACE FUNCTION public.fpo_update()
+         RETURNS text
+         LANGUAGE sql
+1       BEGIN ATOMIC
+2        UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = 'one^1'::text
+3          RETURNING for_portion_of_test.name;
+4       END
+CREATE OR REPLACE function fpo_update()
+RETURNS text
+BEGIN ATOMIC
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01'))
+    SET name = 'one^1'
+    RETURNING name;
+END;
+\sf+ fpo_update()
+        CREATE OR REPLACE FUNCTION public.fpo_update()
+         RETURNS text
+         LANGUAGE sql
+1       BEGIN ATOMIC
+2        UPDATE for_portion_of_test FOR PORTION OF valid_at ((daterange('2018-01-15'::date, '2020-01-01'::date) * daterange('2019-01-01'::date, '2022-01-01'::date))) SET name = 'one^1'::text
+3          RETURNING for_portion_of_test.name;
+4       END
+DROP FUNCTION fpo_update();
+DROP TABLE for_portion_of_test;
+--
+-- DELETE tests
+--
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at daterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three'),
+  ('[4,5)', '(,2018-04-01)', 'four'),
+  ('[5,6)', '(,)', 'five'),
+  ('[6,7)', '[2018-01-01,)', 'six'),
+  ('[7,8)', '(,2018-04-01)', 'seven'),
+  ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'),
+  ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'),
+  ('[8,9)', '[2018-03-03,2018-04-04)', 'eight')
+  ;
+\set QUIET false
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+  WHERE id = '[5,6)';
+ERROR:  column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2:   FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+                         ^
+-- The wrong start type fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+  WHERE id = '[3,4)';
+ERROR:  could not coerce FOR PORTION OF FROM bound from integer to date
+LINE 2:   FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+                                       ^
+-- The wrong end type fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+  WHERE id = '[3,4)';
+ERROR:  could not coerce FOR PORTION OF TO bound from integer to date
+LINE 2:   FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+                                                       ^
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+  WHERE id = '[3,4)';
+ERROR:  range lower bound must be less than or equal to range upper bound
+-- Deleting with a subquery fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+  WHERE id = '[3,4)';
+ERROR:  cannot use subquery in FOR PORTION OF expression
+LINE 2:   FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '201...
+                                       ^
+-- Deleting with a column fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+  WHERE id = '[3,4)';
+ERROR:  cannot use column reference in FOR PORTION OF expression
+LINE 2:   FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+                                             ^
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+  WHERE id = '[3,4)';
+DELETE 0
+-- Deleting a finite/open portion with a finite/open target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  WHERE id = '[3,4)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [3,4) | [2018-01-01,2018-06-01) | three
+(1 row)
+
+-- Deleting a finite/open portion with an open/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+  WHERE id = '[6,7)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at;
+  id   |   valid_at    | name 
+-------+---------------+------
+ [6,7) | [2018-03-01,) | six
+(1 row)
+
+-- Deleting an open/finite portion with an open/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+  WHERE id = '[4,5)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [4,5) | [2018-02-01,2018-04-01) | four
+(1 row)
+
+-- Deleting an open/finite portion with a finite/open target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
+  WHERE id = '[7,8)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at;
+  id   |   valid_at    | name  
+-------+---------------+-------
+ [7,8) | (,2017-01-01) | seven
+(1 row)
+
+-- Deleting a finite/finite portion with an exact fit
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01'
+  WHERE id = '[4,5)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+ id | valid_at | name 
+----+----------+------
+(0 rows)
+
+-- Deleting an enclosed span
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  WHERE id = '[2,3)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+ id | valid_at | name 
+----+----------+------
+(0 rows)
+
+-- Deleting an open/open portion with a finite/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+  WHERE id = '[5,6)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+  id   |   valid_at    | name 
+-------+---------------+------
+ [5,6) | (,2018-01-01) | five
+ [5,6) | [2019-01-01,) | five
+(2 rows)
+
+-- Deleting an enclosed span with separate protruding spans
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03'
+  WHERE id = '[1,2)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-04-04) | one
+(2 rows)
+
+-- Deleting multiple enclosed spans
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  WHERE id = '[8,9)';
+DELETE 3
+SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at;
+ id | valid_at | name 
+----+----------+------
+(0 rows)
+
+-- Deleting with a direct target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15'))
+  WHERE id = '[1,2)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-03-10) | one
+ [1,2) | [2018-03-15,2018-04-04) | one
+(3 rows)
+
+-- Deleting with a direct target, coerced from a string
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)')
+  WHERE id = '[1,2)';
+DELETE 1
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-03-10) | one
+ [1,2) | [2018-03-17,2018-04-04) | one
+(3 rows)
+
+-- Deleting with a direct target of the wrong range subtype fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (int4range(1, 4))
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from int4range to daterange
+LINE 2:   FOR PORTION OF valid_at (int4range(1, 4))
+                                   ^
+-- Deleting with a direct target of a non-rangetype fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (4)
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from integer to daterange
+LINE 2:   FOR PORTION OF valid_at (4)
+                                   ^
+-- Deleting with a direct target of NULL fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (NULL)
+  WHERE id = '[1,2)';
+ERROR:  got a NULL FOR PORTION OF target
+-- Deleting with a direct target of empty does nothing
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at ('empty')
+  WHERE id = '[1,2)';
+DELETE 0
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-03-10) | one
+ [1,2) | [2018-03-17,2018-04-04) | one
+(3 rows)
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2030-01-01' TO NULL;
+DELETE 2
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-03-10) | one
+ [1,2) | [2018-03-17,2018-04-04) | one
+ [3,4) | [2018-01-01,2018-06-01) | three
+ [5,6) | (,2018-01-01)           | five
+ [5,6) | [2019-01-01,2030-01-01) | five
+ [6,7) | [2018-03-01,2030-01-01) | six
+ [7,8) | (,2017-01-01)           | seven
+(8 rows)
+
+\set QUIET true
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+  SET name = 'three^3'
+  WHERE id = '[3,4)'
+  RETURNING *;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [3,4) | [2018-02-01,2018-02-15) | three^3
+(1 row)
+
+-- DELETE FOR PORTION OF with current_date
+-- (We take care not to make the expectation depend on the timestamp.)
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[99,100)', '[2000-01-01,)', 'foo');
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM current_date TO null
+  WHERE id = '[99,100)';
+SELECT name, lower(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date - 1;
+ name |   lower    
+------+------------
+ foo  | 2000-01-01
+(1 row)
+
+SELECT name, upper(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date + 1;
+ name | upper 
+------+-------
+(0 rows)
+
+-- DELETE FOR PORTION OF with clock_timestamp()
+-- fails because the function is volatile:
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM clock_timestamp()::date TO null
+  WHERE id = '[99,100)';
+ERROR:  FOR PORTION OF bounds cannot contain volatile functions
+-- clean up:
+DELETE FROM for_portion_of_test WHERE id = '[99,100)';
+-- DELETE ... RETURNING returns the deleted values (regardless of bounds)
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
+  WHERE id = '[3,4)'
+  RETURNING *;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [3,4) | [2018-02-01,2018-02-15) | three^3
+(1 row)
+
+-- DELETE FOR PORTION OF in a PL/pgSQL function
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date)
+RETURNS void LANGUAGE plpgsql AS
+$$
+BEGIN
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at FROM $2 TO $3
+    WHERE id = $1;
+END;
+$$;
+SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01');
+ fpo_delete 
+------------
+ 
+(1 row)
+
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+   id    |        valid_at         | name 
+---------+-------------------------+------
+ [10,11) | [2019-01-01,2020-01-01) | ten
+(1 row)
+
+DELETE FROM for_portion_of_test WHERE id IN ('[10,11)');
+-- DELETE FOR PORTION OF in a compiled SQL function
+CREATE FUNCTION fpo_delete()
+RETURNS text
+BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+    RETURNING name;
+END;
+\sf+ fpo_delete()
+        CREATE OR REPLACE FUNCTION public.fpo_delete()
+         RETURNS text
+         LANGUAGE sql
+1       BEGIN ATOMIC
+2        DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+3          RETURNING for_portion_of_test.name;
+4       END
+CREATE OR REPLACE function fpo_delete()
+RETURNS text
+BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01'))
+    RETURNING name;
+END;
+\sf+ fpo_delete()
+        CREATE OR REPLACE FUNCTION public.fpo_delete()
+         RETURNS text
+         LANGUAGE sql
+1       BEGIN ATOMIC
+2        DELETE FROM for_portion_of_test FOR PORTION OF valid_at ((daterange('2018-01-15'::date, '2020-01-01'::date) * daterange('2019-01-01'::date, '2022-01-01'::date)))
+3          RETURNING for_portion_of_test.name;
+4       END
+DROP FUNCTION fpo_delete();
+-- test domains and CHECK constraints
+-- With a domain on a rangetype
+CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date);
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at daterange_d,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '[2000-01-01,2020-01-01)', 'one'),
+  (2, '[2000-01-01,2020-01-01)', 'two');
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '[2000-01-01,2005-05-05)', 'nope');
+ERROR:  value for domain daterange_d violates check constraint "daterange_d_check"
+-- UPDATE works:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05'
+  SET name = 'one^1'
+  WHERE id = 1;
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)')
+  SET name = 'one^2'
+  WHERE id = 1;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+ id |        valid_at         | name  
+----+-------------------------+-------
+  1 | [2000-01-01,2010-01-01) | one
+  1 | [2010-01-01,2010-01-05) | one^1
+  1 | [2010-01-05,2010-01-07) | one
+  1 | [2010-01-07,2010-01-09) | one^2
+  1 | [2010-01-09,2020-01-01) | one
+(5 rows)
+
+-- The target is allowed to violate the domain:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  SET name = 'miss'
+  WHERE id = -1;
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)')
+  SET name = 'miss'
+  WHERE id = -1;
+-- test the updated row violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  value for domain daterange_d violates check constraint "daterange_d_check"
+-- test inserts violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01'
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  value for domain daterange_d violates check constraint "daterange_d_check"
+-- test updated row violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11'
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (1, [2000-01-01,2001-01-11), one^3).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02'
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (1, [2002-02-02,2010-01-01), one).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+ id |        valid_at         | name  
+----+-------------------------+-------
+  1 | [2000-01-01,2010-01-01) | one
+  1 | [2010-01-01,2010-01-05) | one^1
+  1 | [2010-01-05,2010-01-07) | one
+  1 | [2010-01-07,2010-01-09) | one^2
+  1 | [2010-01-09,2020-01-01) | one
+(5 rows)
+
+-- DELETE works:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05'
+  WHERE id = 2;
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)')
+  WHERE id = 2;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+ id |        valid_at         | name 
+----+-------------------------+------
+  2 | [2000-01-01,2010-01-01) | two
+  2 | [2010-01-05,2010-01-07) | two
+  2 | [2010-01-09,2020-01-01) | two
+(3 rows)
+
+-- The target is allowed to violate the domain:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  WHERE id = -1;
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)')
+  WHERE id = -1;
+-- test inserts violating the domain
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01'
+  WHERE id = 2;
+ERROR:  value for domain daterange_d violates check constraint "daterange_d_check"
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02');
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02'
+  WHERE id = 2;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (2, [2002-02-02,2010-01-01), two).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+ id |        valid_at         | name 
+----+-------------------------+------
+  2 | [2000-01-01,2010-01-01) | two
+  2 | [2010-01-05,2010-01-07) | two
+  2 | [2010-01-09,2020-01-01) | two
+(3 rows)
+
+DROP TABLE for_portion_of_test2;
+-- With a domain on a multirangetype
+CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$
+  SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r);
+$$;
+CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$
+  SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r);
+$$;
+CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE)));
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at datemultirange_d,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '{[2000-01-01,2020-01-01)}', 'one'),
+  (2, '{[2000-01-01,2020-01-01)}', 'two');
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '{[2000-01-01,2005-05-05)}', 'nope');
+ERROR:  value for domain datemultirange_d violates check constraint "datemultirange_d_check"
+-- UPDATE works:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}')
+  SET name = 'one^2'
+  WHERE id = 1;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+ id |                     valid_at                      | name  
+----+---------------------------------------------------+-------
+  1 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | one
+  1 | {[2010-01-07,2010-01-09)}                         | one^2
+(2 rows)
+
+-- The target is allowed to violate the domain:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  SET name = 'miss'
+  WHERE id = -1;
+-- test the updated row violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  value for domain datemultirange_d violates check constraint "datemultirange_d_check"
+-- test inserts violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  value for domain datemultirange_d violates check constraint "datemultirange_d_check"
+-- test updated row violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (1, {[2000-01-01,2001-01-11)}, one^3).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at)));
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (1, {[2000-01-01,2001-01-01),[2002-02-02,2010-01-07),[2010-01-09,202..., one).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+ id |                     valid_at                      | name  
+----+---------------------------------------------------+-------
+  1 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | one
+  1 | {[2010-01-07,2010-01-09)}                         | one^2
+(2 rows)
+
+-- DELETE works:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}')
+  WHERE id = 2;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+ id |                     valid_at                      | name 
+----+---------------------------------------------------+------
+  2 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | two
+(1 row)
+
+-- The target is allowed to violate the domain:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  WHERE id = -1;
+-- test inserts violating the domain
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}')
+  WHERE id = 2;
+ERROR:  value for domain datemultirange_d violates check constraint "datemultirange_d_check"
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at)));
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}')
+  WHERE id = 2;
+ERROR:  new row for relation "for_portion_of_test2" violates check constraint "fpo2_check"
+DETAIL:  Failing row contains (2, {[2000-01-01,2001-01-01),[2002-02-02,2010-01-07),[2010-01-09,202..., two).
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+ id |                     valid_at                      | name 
+----+---------------------------------------------------+------
+  2 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | two
+(1 row)
+
+DROP TABLE for_portion_of_test2;
+-- test on non-range/multirange columns
+-- With a direct target and a scalar column
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at date,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('2010-01-01')
+  SET name = 'one^1';
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('2010-01-01')
+                         ^
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('2010-01-01');
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('2010-01-01');
+                         ^
+DROP TABLE for_portion_of_test2;
+-- With a direct target and a non-{,multi}range gistable column without overlaps
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at point,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1')
+  SET name = 'one^1';
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('1,1')
+                         ^
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1');
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('1,1');
+                         ^
+DROP TABLE for_portion_of_test2;
+-- With a direct target and a non-{,multi}range column with overlaps
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at box,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1,2,2')
+  SET name = 'one^1';
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('1,1,2,2')
+                         ^
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1,2,2');
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type
+LINE 2:   FOR PORTION OF valid_at ('1,1,2,2');
+                         ^
+DROP TABLE for_portion_of_test2;
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION dump_trigger()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE '%: % % %:',
+    TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+
+  IF TG_ARGV[0] THEN
+    RAISE NOTICE '  old: %', (SELECT string_agg(old_table::text, '\n       ') FROM old_table);
+  ELSE
+    RAISE NOTICE '  old: %', OLD.valid_at;
+  END IF;
+  IF TG_ARGV[1] THEN
+    RAISE NOTICE '  new: %', (SELECT string_agg(new_table::text, '\n       ') FROM new_table);
+  ELSE
+    RAISE NOTICE '  new: %', NEW.valid_at;
+  END IF;
+
+  IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
+    RETURN NEW;
+  ELSIF TG_OP = 'DELETE' THEN
+    RETURN OLD;
+  END IF;
+END;
+$$;
+-- statement triggers:
+CREATE TRIGGER fpo_before_stmt
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_insert_stmt
+AFTER INSERT ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_update_stmt
+AFTER UPDATE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_delete_stmt
+AFTER DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+-- row triggers:
+CREATE TRIGGER fpo_before_row
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+  SET name = 'five^3'
+  WHERE id = '[5,6)';
+NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:    old: [2019-01-01,2030-01-01)
+NOTICE:    new: [2021-01-01,2022-01-01)
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2019-01-01,2021-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2019-01-01,2021-01-01)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2022-01-01,2030-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2022-01-01,2030-01-01)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:    old: [2019-01-01,2030-01-01)
+NOTICE:    new: [2021-01-01,2022-01-01)
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+  WHERE id = '[5,6)';
+NOTICE:  fpo_before_stmt: BEFORE DELETE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE DELETE ROW:
+NOTICE:    old: [2022-01-01,2030-01-01)
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2022-01-01,2023-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2022-01-01,2023-01-01)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2024-01-01,2030-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2024-01-01,2030-01-01)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:    old: [2022-01-01,2030-01-01)
+NOTICE:    new: <NULL>
+NOTICE:  fpo_after_delete_stmt: AFTER DELETE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [2018-01-02,2018-02-03) | one
+ [1,2) | [2018-03-03,2018-03-10) | one
+ [1,2) | [2018-03-17,2018-04-04) | one
+ [3,4) | [2018-01-01,2018-02-01) | three
+ [3,4) | [2018-02-01,2018-02-02) | three^3
+ [3,4) | [2018-02-03,2018-02-15) | three^3
+ [3,4) | [2018-02-15,2018-06-01) | three
+ [5,6) | (,2018-01-01)           | five
+ [5,6) | [2019-01-01,2021-01-01) | five
+ [5,6) | [2021-01-01,2022-01-01) | five^3
+ [5,6) | [2022-01-01,2023-01-01) | five
+ [5,6) | [2024-01-01,2030-01-01) | five
+ [6,7) | [2018-03-01,2030-01-01) | six
+ [7,8) | (,2017-01-01)           | seven
+(14 rows)
+
+-- Triggers with a custom transition table name:
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+-- statement triggers:
+CREATE TRIGGER fpo_before_stmt
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_insert_stmt
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true);
+CREATE TRIGGER fpo_after_update_stmt
+AFTER UPDATE ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true);
+CREATE TRIGGER fpo_after_delete_stmt
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false);
+-- row triggers:
+CREATE TRIGGER fpo_before_row
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true);
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true);
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false);
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:    old: [2018-01-01,2020-01-01)
+NOTICE:    new: [2018-01-15,2019-01-01)
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2018-01-01,2018-01-15)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2019-01-01,2020-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
+ROLLBACK;
+BEGIN;
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+NOTICE:  fpo_before_stmt: BEFORE DELETE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE DELETE ROW:
+NOTICE:    old: [2018-01-01,2020-01-01)
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2018-01-21,2020-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: <NULL>
+NOTICE:  fpo_after_delete_stmt: AFTER DELETE STATEMENT:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: <NULL>
+ROLLBACK;
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:    old: [2018-01-01,2020-01-01)
+NOTICE:    new: [2018-01-01,2018-01-02)
+NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: <NULL>
+NOTICE:  fpo_before_row: BEFORE INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2018-01-02,2020-01-01)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
+NOTICE:    old: <NULL>
+NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+ROLLBACK;
+-- Deferred triggers
+-- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables)
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+CREATE CONSTRAINT TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE CONSTRAINT TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+CREATE CONSTRAINT TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+COMMIT;
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2018-01-01,2018-01-15)
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2019-01-01,2020-01-01)
+NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:    old: [2018-01-01,2020-01-01)
+NOTICE:    new: [2018-01-15,2019-01-01)
+BEGIN;
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+COMMIT;
+NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
+NOTICE:    old: <NULL>
+NOTICE:    new: [2018-01-21,2019-01-01)
+NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:    old: [2018-01-15,2019-01-01)
+NOTICE:    new: <NULL>
+NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:    old: [2018-01-01,2018-01-15)
+NOTICE:    new: <NULL>
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+COMMIT;
+SELECT * FROM for_portion_of_test;
+  id   |        valid_at         |           name           
+-------+-------------------------+--------------------------
+ [1,2) | [2019-01-01,2020-01-01) | one
+ [1,2) | [2018-01-21,2019-01-01) | 2018-01-15_to_2019-01-01
+(2 rows)
+
+-- test FOR PORTION OF from triggers during FOR PORTION OF:
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-01,2020-01-01)', 'one'),
+  ('[2,3)', '[2018-01-01,2020-01-01)', 'two'),
+  ('[3,4)', '[2018-01-01,2020-01-01)', 'three'),
+  ('[4,5)', '[2018-01-01,2020-01-01)', 'four');
+CREATE FUNCTION trg_fpo_update()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  IF pg_trigger_depth() = 1 THEN
+    UPDATE for_portion_of_test
+      FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01'
+      SET name = CONCAT(name, '^')
+      WHERE id = OLD.id;
+  END IF;
+  RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END;
+END;
+$$;
+CREATE FUNCTION trg_fpo_delete()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  IF pg_trigger_depth() = 1 THEN
+    DELETE FROM for_portion_of_test
+      FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01'
+      WHERE id = OLD.id;
+  END IF;
+  RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END;
+END;
+$$;
+-- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '*')
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [2018-01-01,2018-02-01) | one
+ [1,2) | [2018-02-01,2018-03-01) | one^
+ [1,2) | [2018-03-01,2018-05-01) | one
+ [1,2) | [2018-05-01,2018-06-01) | one*
+ [1,2) | [2018-06-01,2020-01-01) | one
+(5 rows)
+
+DROP TRIGGER fpo_after_update_row ON for_portion_of_test;
+-- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [2,3) | [2018-01-01,2018-02-01) | two
+ [2,3) | [2018-02-01,2018-03-01) | two^
+ [2,3) | [2018-03-01,2018-05-01) | two
+ [2,3) | [2018-06-01,2020-01-01) | two
+(4 rows)
+
+DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+-- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '*')
+  WHERE id = '[3,4)';
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+  id   |        valid_at         |  name  
+-------+-------------------------+--------
+ [3,4) | [2018-01-01,2018-03-01) | three
+ [3,4) | [2018-04-01,2018-05-01) | three
+ [3,4) | [2018-05-01,2018-06-01) | three*
+ [3,4) | [2018-06-01,2020-01-01) | three
+(4 rows)
+
+DROP TRIGGER fpo_after_update_row ON for_portion_of_test;
+-- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [4,5) | [2018-01-01,2018-03-01) | four
+ [4,5) | [2018-04-01,2018-05-01) | four
+ [4,5) | [2018-06-01,2020-01-01) | four
+(3 rows)
+
+DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+-- Test with multiranges
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_at datemultirange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'),
+  ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'),
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'),
+  ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three');
+  ;
+-- Updating with FROM/TO
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range type
+LINE 2:   FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+                         ^
+-- Updating with multirange
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01')))
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+  id   |                     valid_at                      | name  
+-------+---------------------------------------------------+-------
+ [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one
+ [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1
+ [1,2) | {[2018-03-03,2018-03-05)}                         | one
+ [1,2) | {[2018-03-05,2018-04-04)}                         | one^1
+(4 rows)
+
+-- Updating with string coercion
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}')
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+  id   |                     valid_at                      | name  
+-------+---------------------------------------------------+-------
+ [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one
+ [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1
+ [1,2) | {[2018-03-03,2018-03-05)}                         | one
+ [1,2) | {[2018-03-05,2018-03-10)}                         | one^2
+ [1,2) | {[2018-03-10,2018-04-04)}                         | one^1
+(5 rows)
+
+-- Updating with the wrong range subtype fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from int4multirange to datemultirange
+LINE 2:   FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+                                   ^
+-- Updating with a non-multirangetype fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (4)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  could not coerce FOR PORTION OF target from integer to datemultirange
+LINE 2:   FOR PORTION OF valid_at (4)
+                                   ^
+-- Updating with NULL fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (NULL)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+ERROR:  got a NULL FOR PORTION OF target
+-- Updating with empty does nothing
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{}')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+  id   |                     valid_at                      | name  
+-------+---------------------------------------------------+-------
+ [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one
+ [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1
+ [1,2) | {[2018-03-03,2018-03-05)}                         | one
+ [1,2) | {[2018-03-05,2018-03-10)}                         | one^2
+ [1,2) | {[2018-03-10,2018-04-04)}                         | one^1
+(5 rows)
+
+-- Deleting with FROM/TO
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+ERROR:  column "valid_at" of relation "for_portion_of_test2" is not a range type
+LINE 2:   FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+                         ^
+-- Deleting with multirange
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at;
+  id   |                                 valid_at                                  | name 
+-------+---------------------------------------------------------------------------+------
+ [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-15,2018-05-01)} | two
+(1 row)
+
+-- Deleting with string coercion
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}')
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at;
+  id   |                                 valid_at                                  | name 
+-------+---------------------------------------------------------------------------+------
+ [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-20,2018-05-01)} | two
+(1 row)
+
+-- Deleting with the wrong range subtype fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+  WHERE id = '[2,3)';
+ERROR:  could not coerce FOR PORTION OF target from int4multirange to datemultirange
+LINE 2:   FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+                                   ^
+-- Deleting with a non-multirangetype fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (4)
+  WHERE id = '[2,3)';
+ERROR:  could not coerce FOR PORTION OF target from integer to datemultirange
+LINE 2:   FOR PORTION OF valid_at (4)
+                                   ^
+-- Deleting with NULL fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (NULL)
+  WHERE id = '[2,3)';
+ERROR:  got a NULL FOR PORTION OF target
+-- Deleting with empty does nothing
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{}')
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+  id   |                                 valid_at                                  | name  
+-------+---------------------------------------------------------------------------+-------
+ [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)}                         | one
+ [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)}                         | one^1
+ [1,2) | {[2018-03-03,2018-03-05)}                                                 | one
+ [1,2) | {[2018-03-05,2018-03-10)}                                                 | one^2
+ [1,2) | {[2018-03-10,2018-04-04)}                                                 | one^1
+ [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-20,2018-05-01)} | two
+ [3,4) | {[2018-01-01,)}                                                           | three
+(7 rows)
+
+DROP TABLE for_portion_of_test2;
+-- Test with a custom range type
+CREATE TYPE mydaterange AS range(subtype=date);
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_at mydaterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-05-01)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three');
+  ;
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2018-01-02,2018-01-10) | one
+ [1,2) | [2018-01-10,2018-02-03) | one^1
+ [1,2) | [2018-02-03,2018-02-10) | one^1
+ [1,2) | [2018-02-10,2018-03-03) | one
+ [1,2) | [2018-03-03,2018-04-04) | one
+ [2,3) | [2018-01-01,2018-01-15) | two
+ [2,3) | [2018-02-15,2018-05-01) | two
+ [3,4) | [2018-01-01,)           | three
+(8 rows)
+
+DROP TABLE for_portion_of_test2;
+DROP TYPE mydaterange;
+-- Test FOR PORTION OF against a partitioned table.
+-- temporal_partitioned_1 has the same attnums as the root
+-- temporal_partitioned_3 has the different attnums from the root
+-- temporal_partitioned_5 has the different attnums too, but reversed
+CREATE TABLE temporal_partitioned (
+  id int4range,
+  valid_at daterange,
+  name text,
+  CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)');
+ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3;
+ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at;
+ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL;
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)');
+ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5;
+ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at;
+ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL;
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)');
+INSERT INTO temporal_partitioned (id, valid_at, name) VALUES
+  ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'),
+  ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'),
+  ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five');
+SELECT * FROM temporal_partitioned;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2000-01-01,2010-01-01) | one
+ [3,4) | [2000-01-01,2010-01-01) | three
+ [5,6) | [2000-01-01,2010-01-01) | five
+(3 rows)
+
+-- Update without moving within partition 1
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+-- Update without moving within partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+-- Update without moving within partition 5
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'five^1'
+  WHERE id = '[5,6)';
+-- Move from partition 1 to partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'one^2',
+      id = '[4,5)'
+  WHERE id = '[1,2)';
+-- Move from partition 3 to partition 1
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'three^2',
+      id = '[2,3)'
+  WHERE id = '[3,4)';
+-- Move from partition 5 to partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'five^2',
+      id = '[3,4)'
+  WHERE id = '[5,6)';
+-- Update all partitions at once (each with leftovers)
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [2000-01-01,2000-03-01) | one
+ [1,2) | [2000-03-01,2000-04-01) | one^1
+ [1,2) | [2000-04-01,2000-06-01) | one
+ [1,2) | [2000-07-01,2010-01-01) | one
+ [2,3) | [2000-06-01,2000-07-01) | three^2
+ [3,4) | [2000-01-01,2000-03-01) | three
+ [3,4) | [2000-03-01,2000-04-01) | three^1
+ [3,4) | [2000-04-01,2000-06-01) | three
+ [3,4) | [2000-06-01,2000-07-01) | five^2
+ [3,4) | [2000-07-01,2010-01-01) | three
+ [4,5) | [2000-06-01,2000-07-01) | one^2
+ [5,6) | [2000-01-01,2000-03-01) | five
+ [5,6) | [2000-03-01,2000-04-01) | five^1
+ [5,6) | [2000-04-01,2000-06-01) | five
+ [5,6) | [2000-07-01,2010-01-01) | five
+(15 rows)
+
+SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [2000-01-01,2000-03-01) | one
+ [1,2) | [2000-03-01,2000-04-01) | one^1
+ [1,2) | [2000-04-01,2000-06-01) | one
+ [1,2) | [2000-07-01,2010-01-01) | one
+ [2,3) | [2000-06-01,2000-07-01) | three^2
+(5 rows)
+
+SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at;
+  name   |  id   |        valid_at         
+---------+-------+-------------------------
+ three   | [3,4) | [2000-01-01,2000-03-01)
+ three^1 | [3,4) | [2000-03-01,2000-04-01)
+ three   | [3,4) | [2000-04-01,2000-06-01)
+ five^2  | [3,4) | [2000-06-01,2000-07-01)
+ three   | [3,4) | [2000-07-01,2010-01-01)
+ one^2   | [4,5) | [2000-06-01,2000-07-01)
+(6 rows)
+
+SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
+  name  |        valid_at         |  id   
+--------+-------------------------+-------
+ five   | [2000-01-01,2000-03-01) | [5,6)
+ five^1 | [2000-03-01,2000-04-01) | [5,6)
+ five   | [2000-04-01,2000-06-01) | [5,6)
+ five   | [2000-07-01,2010-01-01) | [5,6)
+(4 rows)
+
+DROP TABLE temporal_partitioned;
+RESET datestyle;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 84c1c1ca38d..8272b67a693 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -1145,6 +1145,34 @@ ERROR:  null value in column "b" of relation "errtst_part_2" violates not-null c
 DETAIL:  Failing row contains (a, b, c) = (aaaa, null, ccc).
 SET SESSION AUTHORIZATION regress_priv_user1;
 DROP TABLE errtst;
+-- test column-level privileges on the range used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+  c1 int4range,
+  valid_at tsrange,
+	CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+-- UPDATE requires select permission on the valid_at column (but not update):
+GRANT SELECT (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+ERROR:  permission denied for table t1
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+-- DELETE requires select permission on the valid_at column:
+GRANT DELETE ON t1 TO regress_priv_user2;
+GRANT DELETE ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01';
+ERROR:  permission denied for table t1
+SET SESSION AUTHORIZATION regress_priv_user3;
+DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
 -- test column-level privileges when involved with DELETE
 SET SESSION AUTHORIZATION regress_priv_user1;
 ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 9cea538b8e8..8852160718f 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3722,6 +3722,38 @@ select * from uv_iocu_tab;
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+    constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+    select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view order by id, valid_at;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+(2 rows)
+
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view order by id, valid_at;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+(3 rows)
+
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view order by id, valid_at;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [1,2) | 2.0
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+(3 rows)
+
 -- Test whole-row references to the view
 create table uv_iocu_tab (a int unique, b text);
 create view uv_iocu_view as
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 06f6fd2c8c5..73b2c78a4ce 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -2,7 +2,7 @@
 --
 -- We leave behind several tables to test pg_dump etc:
 -- temporal_rng, temporal_rng2,
--- temporal_fk_rng2rng.
+-- temporal_fk_rng2rng, temporal_fk2_rng2rng.
 SET datestyle TO ISO, YMD;
 --
 -- test input parser
@@ -889,6 +889,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
   ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'),
   ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar')
 ;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+  SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+  SET name = name || '2'
+  WHERE id = '[2,3)';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         |  id2   | name  
+-------+-------------------------+--------+-------
+ [1,2) | [2000-01-01,2000-05-01) | [7,8)  | foo
+ [1,2) | [2000-05-01,2000-07-01) | [7,8)  | foo1
+ [1,2) | [2000-07-01,2010-01-01) | [7,8)  | foo
+ [2,3) | [2000-01-01,2000-04-01) | [9,10) | bar
+ [2,3) | [2000-04-01,2000-05-01) | [9,10) | bar2
+ [2,3) | [2000-05-01,2000-06-01) | [9,10) | bar12
+ [2,3) | [2000-06-01,2000-07-01) | [9,10) | bar1
+ [2,3) | [2000-07-01,2010-01-01) | [9,10) | bar
+(8 rows)
+
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,2)', daterange('2005-01-01', '2006-01-01'), '[8,9)', 'foo3');
+ERROR:  conflicting key value violates exclusion constraint "temporal3_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2005-01-01,2006-01-01)) conflicts with existing key (id, valid_at)=([1,2), [2000-07-01,2010-01-01)).
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[3,4)', daterange('2005-01-01', '2010-01-01'), '[9,10)', 'bar3');
+ERROR:  conflicting key value violates exclusion constraint "temporal3_uniq"
+DETAIL:  Key (id2, valid_at)=([9,10), [2005-01-01,2010-01-01)) conflicts with existing key (id2, valid_at)=([9,10), [2000-07-01,2010-01-01)).
 DROP TABLE temporal3;
 --
 -- test changing the PK's dependencies
@@ -920,26 +950,43 @@ INSERT INTO temporal_partitioned (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'),
   ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three');
-SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
-  id   |        valid_at         | name  
--------+-------------------------+-------
- [1,2) | [2000-01-01,2000-02-01) | one
- [1,2) | [2000-02-01,2000-03-01) | one
- [3,4) | [2000-01-01,2010-01-01) | three
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+ tableoid |  id   |        valid_at         | name  
+----------+-------+-------------------------+-------
+ tp1      | [1,2) | [2000-01-01,2000-02-01) | one
+ tp1      | [1,2) | [2000-02-01,2000-03-01) | one
+ tp2      | [3,4) | [2000-01-01,2010-01-01) | three
 (3 rows)
 
-SELECT * FROM tp1 ORDER BY id, valid_at;
-  id   |        valid_at         | name 
--------+-------------------------+------
- [1,2) | [2000-01-01,2000-02-01) | one
- [1,2) | [2000-02-01,2000-03-01) | one
-(2 rows)
-
-SELECT * FROM tp2 ORDER BY id, valid_at;
-  id   |        valid_at         | name  
--------+-------------------------+-------
- [3,4) | [2000-01-01,2010-01-01) | three
-(1 row)
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,2)';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,5)'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,3)'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,4)';
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+ tableoid |  id   |        valid_at         | name  
+----------+-------+-------------------------+-------
+ tp1      | [1,2) | [2000-01-01,2000-01-15) | one
+ tp1      | [1,2) | [2000-01-15,2000-02-01) | one2
+ tp1      | [1,2) | [2000-02-01,2000-02-15) | one2
+ tp1      | [1,2) | [2000-02-15,2000-02-20) | one
+ tp1      | [1,2) | [2000-02-25,2000-03-01) | one
+ tp1      | [2,3) | [2002-01-01,2003-01-01) | three
+ tp2      | [3,4) | [2000-01-01,2000-01-15) | three
+ tp2      | [3,4) | [2000-02-15,2002-01-01) | three
+ tp2      | [3,4) | [2003-01-01,2010-01-01) | three
+ tp2      | [4,5) | [2000-02-20,2000-02-25) | one
+(10 rows)
 
 DROP TABLE temporal_partitioned;
 -- temporal UNIQUE:
@@ -955,26 +1002,43 @@ INSERT INTO temporal_partitioned (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'),
   ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three');
-SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
-  id   |        valid_at         | name  
--------+-------------------------+-------
- [1,2) | [2000-01-01,2000-02-01) | one
- [1,2) | [2000-02-01,2000-03-01) | one
- [3,4) | [2000-01-01,2010-01-01) | three
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+ tableoid |  id   |        valid_at         | name  
+----------+-------+-------------------------+-------
+ tp1      | [1,2) | [2000-01-01,2000-02-01) | one
+ tp1      | [1,2) | [2000-02-01,2000-03-01) | one
+ tp2      | [3,4) | [2000-01-01,2010-01-01) | three
 (3 rows)
 
-SELECT * FROM tp1 ORDER BY id, valid_at;
-  id   |        valid_at         | name 
--------+-------------------------+------
- [1,2) | [2000-01-01,2000-02-01) | one
- [1,2) | [2000-02-01,2000-03-01) | one
-(2 rows)
-
-SELECT * FROM tp2 ORDER BY id, valid_at;
-  id   |        valid_at         | name  
--------+-------------------------+-------
- [3,4) | [2000-01-01,2010-01-01) | three
-(1 row)
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,2)';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,5)'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,3)'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,4)';
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+ tableoid |  id   |        valid_at         | name  
+----------+-------+-------------------------+-------
+ tp1      | [1,2) | [2000-01-01,2000-01-15) | one
+ tp1      | [1,2) | [2000-01-15,2000-02-01) | one2
+ tp1      | [1,2) | [2000-02-01,2000-02-15) | one2
+ tp1      | [1,2) | [2000-02-15,2000-02-20) | one
+ tp1      | [1,2) | [2000-02-25,2000-03-01) | one
+ tp1      | [2,3) | [2002-01-01,2003-01-01) | three
+ tp2      | [3,4) | [2000-01-01,2000-01-15) | three
+ tp2      | [3,4) | [2000-02-15,2002-01-01) | three
+ tp2      | [3,4) | [2003-01-01,2010-01-01) | three
+ tp2      | [4,5) | [2000-02-20,2000-02-25) | one
+(10 rows)
 
 DROP TABLE temporal_partitioned;
 -- ALTER TABLE REPLICA IDENTITY
@@ -1755,6 +1819,33 @@ UPDATE temporal_rng SET id = '[7,8)'
   WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
 ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+-- changing an unreferenced part is okay:
+UPDATE temporal_rng
+  FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+-- changing just a part fails:
+UPDATE temporal_rng
+  FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [5,6) | [2016-02-01,2016-03-01)
+ [5,6) | [2018-01-01,2018-01-02)
+ [5,6) | [2018-01-03,2018-02-01)
+ [7,8) | [2018-01-02,2018-01-03)
+(4 rows)
+
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [3,4) | [2018-01-05,2018-01-10) | [5,6)
+(1 row)
+
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)';
 UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
@@ -1802,6 +1893,42 @@ BEGIN;
 COMMIT;
 ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [5,6) | [2018-01-01,2018-01-02)
+ [5,6) | [2018-01-03,2018-02-01)
+(2 rows)
+
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [3,4) | [2018-01-05,2018-01-10) | [5,6)
+(1 row)
+
+-- deleting just a part fails:
+DELETE FROM temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,6)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [5,6) | [2018-01-01,2018-01-02)
+ [5,6) | [2018-01-03,2018-02-01)
+(2 rows)
+
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [3,4) | [2018-01-05,2018-01-10) | [5,6)
+(1 row)
+
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)';
 DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
@@ -1818,11 +1945,12 @@ ALTER TABLE temporal_fk_rng2rng
   ON DELETE RESTRICT;
 ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
 --
--- test ON UPDATE/DELETE options
+-- rng2rng test ON UPDATE/DELETE options
 --
 -- 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 ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
+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
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
@@ -1830,8 +1958,9 @@ ALTER TABLE temporal_fk_rng2rng
     ON DELETE CASCADE ON UPDATE CASCADE;
 ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
 -- test FK referenced updates SET NULL
-INSERT INTO temporal_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)');
+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
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
@@ -1839,9 +1968,10 @@ ALTER TABLE temporal_fk_rng2rng
     ON DELETE SET NULL ON UPDATE SET NULL;
 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));
-INSERT INTO temporal_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)');
+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
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
   ADD CONSTRAINT temporal_fk_rng2rng_fk
@@ -2211,6 +2341,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
   WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 ERROR:  update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng"
 DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- changing an unreferenced part is okay:
+UPDATE temporal_mltrng
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+-- changing just a part fails:
+UPDATE temporal_mltrng
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+ERROR:  update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)';
+UPDATE temporal_mltrng SET id = '[7,8)'
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 --
 -- test FK referenced updates RESTRICT
 --
@@ -2253,6 +2399,19 @@ BEGIN;
 COMMIT;
 ERROR:  update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng"
 DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+WHERE id = '[5,6)';
+-- deleting just a part fails:
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+WHERE id = '[5,6)';
+ERROR:  update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- 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'));
 --
 -- FK between partitioned tables: ranges
 --
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 549e9b2d7be..38e5def9062 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
new file mode 100644
index 00000000000..72fb5273077
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,1356 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+
+SET datestyle TO ISO, YMD;
+
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = 'one^1';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- With a table alias with AS
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t
+  SET name = 'one^2';
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t;
+
+-- With a table alias without AS
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t
+  SET name = 'one^3';
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t;
+
+-- UPDATE with FROM
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02'
+  SET name = 'one^4'
+  FROM (SELECT '[1,2)'::int4range) AS t2(id)
+  WHERE for_portion_of_test.id = t2.id;
+
+-- DELETE with USING
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03'
+  USING (SELECT '[1,2)'::int4range) AS t2(id)
+  WHERE for_portion_of_test.id = t2.id;
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid1_at daterange,
+  valid2_at daterange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL
+  SET name = 'foo';
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL
+  SET name = 'bar';
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+
+-- Test with NULLs in the scalar/range key columns.
+-- This won't happen if there is a PRIMARY KEY or UNIQUE constraint
+-- but FOR PORTION OF shouldn't require that.
+DROP TABLE for_portion_of_test;
+CREATE UNLOGGED TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', NULL, '1 null'),
+  ('[1,2)', '(,)', '1 unbounded'),
+  ('[1,2)', 'empty', '1 empty'),
+  (NULL, NULL, NULL),
+  (NULL, daterange('2018-01-01', '2019-01-01'), 'null key');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'NULL to NULL';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+DROP TABLE for_portion_of_test;
+
+--
+-- UPDATE tests
+--
+
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at daterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three'),
+  ('[4,5)', '(,2018-04-01)', 'four'),
+  ('[5,6)', '(,)', 'five')
+  ;
+\set QUIET false
+
+-- Updating with a missing column fails
+UPDATE for_portion_of_test
+  FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+  SET name = 'foo'
+  WHERE id = '[5,6)';
+
+-- Updating the range fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  SET valid_at = '[1990-01-01,1999-01-01)'
+  WHERE id = '[5,6)';
+
+-- The wrong start type fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+
+-- The wrong end type fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+
+-- Updating with timestamps reversed fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+
+-- Updating with a subquery fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+
+-- Updating with a column fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+  SET name = 'nope'
+  WHERE id = '[3,4)';
+
+-- Updating with timestamps equal does nothing
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+  SET name = 'three^0'
+  WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+  SET name = 'three^2'
+  WHERE id = '[3,4)';
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+  SET name = 'four^1'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
+  SET name = 'four^2'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+  SET name = 'four^3'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'two^2'
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+  SET name = 'five^1'
+  WHERE id = '[5,6)';
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+  SET name = 'five^2'
+  WHERE id = '[5,6)';
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Updating with a direct target
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15'))
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Updating with a direct target, coerced from a string
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Updating with a direct target of the wrong range subtype fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (int4range(1, 4))
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+
+-- Updating with a direct target of a non-rangetype fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (4)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+
+-- Updating with a direct target of NULL fails
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at (NULL)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+
+-- Updating with a direct target of empty does nothing
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at ('empty')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-15' TO NULL
+  SET id = '[6,7)'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2030-01-01' TO NULL
+  SET name = name || '*';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+\set QUIET true
+
+-- Updating with a shift/reduce conflict
+-- (requires a tsrange column)
+CREATE UNLOGGED TABLE for_portion_of_test2 (
+  id int4range,
+  valid_at tsrange,
+  name text
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', '[2000-01-01,2020-01-01)', 'one');
+-- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes)
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE
+    TO '2012-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+
+-- TO is used for the bound but not the INTERVAL:
+-- syntax error
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR
+    TO '2014-01-01'
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+
+-- adding parens fixes it
+-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes)
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at
+    FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR)
+    TO '2016-01-01'
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+DROP TABLE for_portion_of_test2;
+
+-- UPDATE FOR PORTION OF in a CTE:
+
+-- Visible to SELECT:
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+WITH update_apr AS (
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01'
+    SET name = 'Apr 2018'
+    WHERE id = '[10,11)'
+    RETURNING id, valid_at, name
+)
+SELECT *
+  FROM for_portion_of_test AS t, update_apr
+  WHERE t.id = update_apr.id;
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+
+-- UPDATE FOR PORTION OF with current_date
+-- (We take care not to make the expectation depend on the timestamp.)
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[99,100)', '[2000-01-01,)', 'foo');
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM current_date TO null
+  SET name = 'bar'
+  WHERE id = '[99,100)';
+SELECT name, lower(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date - 1;
+SELECT name, upper(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date + 1;
+
+-- UPDATE FOR PORTION OF with clock_timestamp()
+-- fails because the function is volatile:
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM clock_timestamp()::date TO null
+  SET name = 'baz'
+  WHERE id = '[99,100)';
+
+-- clean up:
+DELETE FROM for_portion_of_test WHERE id = '[99,100)';
+
+-- Not visible to UPDATE:
+-- Tuples updated/inserted within the CTE are not visible to the main query yet,
+-- but neither are old tuples the CTE changed:
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven');
+WITH update_apr AS (
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01'
+    SET name = 'Apr 2018'
+    WHERE id = '[11,12)'
+    RETURNING id, valid_at, name
+)
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  AS t
+  SET name = 'May 2018'
+  FROM update_apr AS j
+  WHERE t.id = j.id;
+SELECT * FROM for_portion_of_test WHERE id = '[11,12)';
+DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)');
+
+-- UPDATE FOR PORTION OF in a PL/pgSQL function
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date)
+RETURNS void LANGUAGE plpgsql AS
+$$
+BEGIN
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM $2 TO $3
+    SET name = concat(_target_from::text, ' to ', _target_til::text)
+    WHERE id = $1;
+END;
+$$;
+SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01');
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+
+-- UPDATE FOR PORTION OF in a compiled SQL function
+CREATE FUNCTION fpo_update()
+RETURNS text
+BEGIN ATOMIC
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+    SET name = 'one^1'
+    RETURNING name;
+END;
+\sf+ fpo_update()
+CREATE OR REPLACE function fpo_update()
+RETURNS text
+BEGIN ATOMIC
+  UPDATE for_portion_of_test
+    FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01'))
+    SET name = 'one^1'
+    RETURNING name;
+END;
+\sf+ fpo_update()
+DROP FUNCTION fpo_update();
+
+DROP TABLE for_portion_of_test;
+
+--
+-- DELETE tests
+--
+
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at daterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three'),
+  ('[4,5)', '(,2018-04-01)', 'four'),
+  ('[5,6)', '(,)', 'five'),
+  ('[6,7)', '[2018-01-01,)', 'six'),
+  ('[7,8)', '(,2018-04-01)', 'seven'),
+  ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'),
+  ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'),
+  ('[8,9)', '[2018-03-03,2018-04-04)', 'eight')
+  ;
+\set QUIET false
+
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL
+  WHERE id = '[5,6)';
+
+-- The wrong start type fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM 1 TO '2020-01-01'
+  WHERE id = '[3,4)';
+
+-- The wrong end type fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2000-01-01' TO 4
+  WHERE id = '[3,4)';
+
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+  WHERE id = '[3,4)';
+
+-- Deleting with a subquery fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01'
+  WHERE id = '[3,4)';
+
+-- Deleting with a column fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+  WHERE id = '[3,4)';
+
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+  WHERE id = '[3,4)';
+
+-- Deleting a finite/open portion with a finite/open target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+  WHERE id = '[3,4)';
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+
+-- Deleting a finite/open portion with an open/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+  WHERE id = '[6,7)';
+SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at;
+
+-- Deleting an open/finite portion with an open/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+-- Deleting an open/finite portion with a finite/open target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
+  WHERE id = '[7,8)';
+SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at;
+
+-- Deleting a finite/finite portion with an exact fit
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01'
+  WHERE id = '[4,5)';
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+-- Deleting an enclosed span
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+
+-- Deleting an open/open portion with a finite/finite target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+  WHERE id = '[5,6)';
+SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at;
+
+-- Deleting an enclosed span with separate protruding spans
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Deleting multiple enclosed spans
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  WHERE id = '[8,9)';
+SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at;
+
+-- Deleting with a direct target
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15'))
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Deleting with a direct target, coerced from a string
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)')
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- Deleting with a direct target of the wrong range subtype fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (int4range(1, 4))
+  WHERE id = '[1,2)';
+
+-- Deleting with a direct target of a non-rangetype fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (4)
+  WHERE id = '[1,2)';
+
+-- Deleting with a direct target of NULL fails
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at (NULL)
+  WHERE id = '[1,2)';
+
+-- Deleting with a direct target of empty does nothing
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at ('empty')
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2030-01-01' TO NULL;
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+\set QUIET true
+
+-- UPDATE ... RETURNING returns only the updated values (not the inserted side values)
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15'
+  SET name = 'three^3'
+  WHERE id = '[3,4)'
+  RETURNING *;
+
+-- DELETE FOR PORTION OF with current_date
+-- (We take care not to make the expectation depend on the timestamp.)
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[99,100)', '[2000-01-01,)', 'foo');
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM current_date TO null
+  WHERE id = '[99,100)';
+SELECT name, lower(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date - 1;
+SELECT name, upper(valid_at) FROM for_portion_of_test
+  WHERE id = '[99,100)' AND valid_at @> current_date + 1;
+
+-- DELETE FOR PORTION OF with clock_timestamp()
+-- fails because the function is volatile:
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM clock_timestamp()::date TO null
+  WHERE id = '[99,100)';
+
+-- clean up:
+DELETE FROM for_portion_of_test WHERE id = '[99,100)';
+
+-- DELETE ... RETURNING returns the deleted values (regardless of bounds)
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
+  WHERE id = '[3,4)'
+  RETURNING *;
+
+-- DELETE FOR PORTION OF in a PL/pgSQL function
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[10,11)', '[2018-01-01,2020-01-01)', 'ten');
+CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date)
+RETURNS void LANGUAGE plpgsql AS
+$$
+BEGIN
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at FROM $2 TO $3
+    WHERE id = $1;
+END;
+$$;
+SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01');
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)';
+DELETE FROM for_portion_of_test WHERE id IN ('[10,11)');
+
+-- DELETE FOR PORTION OF in a compiled SQL function
+CREATE FUNCTION fpo_delete()
+RETURNS text
+BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+    RETURNING name;
+END;
+\sf+ fpo_delete()
+CREATE OR REPLACE function fpo_delete()
+RETURNS text
+BEGIN ATOMIC
+  DELETE FROM for_portion_of_test
+    FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01'))
+    RETURNING name;
+END;
+\sf+ fpo_delete()
+DROP FUNCTION fpo_delete();
+
+
+-- test domains and CHECK constraints
+
+-- With a domain on a rangetype
+CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date);
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at daterange_d,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '[2000-01-01,2020-01-01)', 'one'),
+  (2, '[2000-01-01,2020-01-01)', 'two');
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '[2000-01-01,2005-05-05)', 'nope');
+-- UPDATE works:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05'
+  SET name = 'one^1'
+  WHERE id = 1;
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)')
+  SET name = 'one^2'
+  WHERE id = 1;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+-- The target is allowed to violate the domain:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  SET name = 'miss'
+  WHERE id = -1;
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)')
+  SET name = 'miss'
+  WHERE id = -1;
+-- test the updated row violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  SET name = 'one^3'
+  WHERE id = 1;
+-- test inserts violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01'
+  SET name = 'one^3'
+  WHERE id = 1;
+-- test updated row violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11'
+  SET name = 'one^3'
+  WHERE id = 1;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02'
+  SET name = 'one^3'
+  WHERE id = 1;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+-- DELETE works:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05'
+  WHERE id = 2;
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)')
+  WHERE id = 2;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+-- The target is allowed to violate the domain:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05'
+  WHERE id = -1;
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)')
+  WHERE id = -1;
+-- test inserts violating the domain
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01'
+  WHERE id = 2;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02');
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02'
+  WHERE id = 2;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+DROP TABLE for_portion_of_test2;
+
+-- With a domain on a multirangetype
+CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$
+  SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r);
+$$;
+CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$
+  SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r);
+$$;
+CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE)));
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at datemultirange_d,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '{[2000-01-01,2020-01-01)}', 'one'),
+  (2, '{[2000-01-01,2020-01-01)}', 'two');
+INSERT INTO for_portion_of_test2 VALUES
+  (1, '{[2000-01-01,2005-05-05)}', 'nope');
+-- UPDATE works:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}')
+  SET name = 'one^2'
+  WHERE id = 1;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+-- The target is allowed to violate the domain:
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  SET name = 'miss'
+  WHERE id = -1;
+-- test the updated row violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+-- test inserts violating the domain
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+-- test updated row violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at)));
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}')
+  SET name = 'one^3'
+  WHERE id = 1;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at;
+-- DELETE works:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}')
+  WHERE id = 2;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+-- The target is allowed to violate the domain:
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}')
+  WHERE id = -1;
+-- test inserts violating the domain
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}')
+  WHERE id = 2;
+-- test inserts violating CHECK constraints
+ALTER TABLE for_portion_of_test2
+  ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at)));
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}')
+  WHERE id = 2;
+ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check;
+SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at;
+DROP TABLE for_portion_of_test2;
+
+-- test on non-range/multirange columns
+
+-- With a direct target and a scalar column
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at date,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('2010-01-01')
+  SET name = 'one^1';
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('2010-01-01');
+DROP TABLE for_portion_of_test2;
+
+-- With a direct target and a non-{,multi}range gistable column without overlaps
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at point,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1')
+  SET name = 'one^1';
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1');
+DROP TABLE for_portion_of_test2;
+
+-- With a direct target and a non-{,multi}range column with overlaps
+CREATE TABLE for_portion_of_test2 (
+  id integer,
+  valid_at box,
+  name text
+);
+INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one');
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1,2,2')
+  SET name = 'one^1';
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('1,1,2,2');
+DROP TABLE for_portion_of_test2;
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+
+CREATE FUNCTION dump_trigger()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  RAISE NOTICE '%: % % %:',
+    TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+
+  IF TG_ARGV[0] THEN
+    RAISE NOTICE '  old: %', (SELECT string_agg(old_table::text, '\n       ') FROM old_table);
+  ELSE
+    RAISE NOTICE '  old: %', OLD.valid_at;
+  END IF;
+  IF TG_ARGV[1] THEN
+    RAISE NOTICE '  new: %', (SELECT string_agg(new_table::text, '\n       ') FROM new_table);
+  ELSE
+    RAISE NOTICE '  new: %', NEW.valid_at;
+  END IF;
+
+  IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
+    RETURN NEW;
+  ELSIF TG_OP = 'DELETE' THEN
+    RETURN OLD;
+  END IF;
+END;
+$$;
+
+-- statement triggers:
+
+CREATE TRIGGER fpo_before_stmt
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_insert_stmt
+AFTER INSERT ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_update_stmt
+AFTER UPDATE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_delete_stmt
+AFTER DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+
+-- row triggers:
+
+CREATE TRIGGER fpo_before_row
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+  SET name = 'five^3'
+  WHERE id = '[5,6)';
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+  WHERE id = '[5,6)';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- Triggers with a custom transition table name:
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+
+-- statement triggers:
+
+CREATE TRIGGER fpo_before_stmt
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_insert_stmt
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true);
+
+CREATE TRIGGER fpo_after_update_stmt
+AFTER UPDATE ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true);
+
+CREATE TRIGGER fpo_after_delete_stmt
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false);
+
+-- row triggers:
+
+CREATE TRIGGER fpo_before_row
+BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true);
+
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true);
+
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false);
+
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+ROLLBACK;
+
+BEGIN;
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+ROLLBACK;
+
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+ROLLBACK;
+
+-- Deferred triggers
+-- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables)
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+
+CREATE CONSTRAINT TRIGGER fpo_after_insert_row
+AFTER INSERT ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE CONSTRAINT TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+CREATE CONSTRAINT TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+DEFERRABLE INITIALLY DEFERRED
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+COMMIT;
+
+BEGIN;
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+COMMIT;
+
+BEGIN;
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+COMMIT;
+
+SELECT * FROM for_portion_of_test;
+
+-- test FOR PORTION OF from triggers during FOR PORTION OF:
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at daterange,
+  name text
+);
+INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-01,2020-01-01)', 'one'),
+  ('[2,3)', '[2018-01-01,2020-01-01)', 'two'),
+  ('[3,4)', '[2018-01-01,2020-01-01)', 'three'),
+  ('[4,5)', '[2018-01-01,2020-01-01)', 'four');
+
+CREATE FUNCTION trg_fpo_update()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  IF pg_trigger_depth() = 1 THEN
+    UPDATE for_portion_of_test
+      FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01'
+      SET name = CONCAT(name, '^')
+      WHERE id = OLD.id;
+  END IF;
+  RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END;
+END;
+$$;
+
+CREATE FUNCTION trg_fpo_delete()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  IF pg_trigger_depth() = 1 THEN
+    DELETE FROM for_portion_of_test
+      FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01'
+      WHERE id = OLD.id;
+  END IF;
+  RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END;
+END;
+$$;
+
+-- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF
+
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '*')
+  WHERE id = '[1,2)';
+
+SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at;
+
+DROP TRIGGER fpo_after_update_row ON for_portion_of_test;
+
+-- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF
+
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  WHERE id = '[2,3)';
+
+SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at;
+
+DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+
+-- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF
+
+CREATE TRIGGER fpo_after_update_row
+AFTER UPDATE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '*')
+  WHERE id = '[3,4)';
+
+SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at;
+
+DROP TRIGGER fpo_after_update_row ON for_portion_of_test;
+
+-- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF
+
+CREATE TRIGGER fpo_after_delete_row
+AFTER DELETE ON for_portion_of_test
+FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+
+DELETE FROM for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  WHERE id = '[4,5)';
+
+SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
+
+DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+
+-- Test with multiranges
+
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_at datemultirange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'),
+  ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'),
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'),
+  ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three');
+  ;
+
+-- Updating with FROM/TO
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+-- Updating with multirange
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01')))
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+-- Updating with string coercion
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}')
+  SET name = 'one^2'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+-- Updating with the wrong range subtype fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+-- Updating with a non-multirangetype fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (4)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+-- Updating with NULL fails
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at (NULL)
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+-- Updating with empty does nothing
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at ('{}')
+  SET name = 'one^3'
+  WHERE id = '[1,2)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at;
+
+-- Deleting with FROM/TO
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+-- Deleting with multirange
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at;
+-- Deleting with string coercion
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}')
+  WHERE id = '[2,3)';
+SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at;
+-- Deleting with the wrong range subtype fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{[1,4)}'::int4multirange)
+  WHERE id = '[2,3)';
+-- Deleting with a non-multirangetype fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (4)
+  WHERE id = '[2,3)';
+-- Deleting with NULL fails
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at (NULL)
+  WHERE id = '[2,3)';
+-- Deleting with empty does nothing
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at ('{}')
+  WHERE id = '[2,3)';
+
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+
+DROP TABLE for_portion_of_test2;
+
+-- Test with a custom range type
+
+CREATE TYPE mydaterange AS range(subtype=date);
+
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_at mydaterange NOT NULL,
+  name text NOT NULL,
+  CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+  ('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+  ('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+  ('[2,3)', '[2018-01-01,2018-05-01)', 'two'),
+  ('[3,4)', '[2018-01-01,)', 'three');
+  ;
+
+UPDATE for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+
+DELETE FROM for_portion_of_test2
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+  WHERE id = '[2,3)';
+
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+
+DROP TABLE for_portion_of_test2;
+DROP TYPE mydaterange;
+
+-- Test FOR PORTION OF against a partitioned table.
+-- temporal_partitioned_1 has the same attnums as the root
+-- temporal_partitioned_3 has the different attnums from the root
+-- temporal_partitioned_5 has the different attnums too, but reversed
+
+CREATE TABLE temporal_partitioned (
+  id int4range,
+  valid_at daterange,
+  name text,
+  CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)');
+
+ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3;
+ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at;
+ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL;
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)');
+
+ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5;
+ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at;
+ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL;
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)');
+
+INSERT INTO temporal_partitioned (id, valid_at, name) VALUES
+  ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'),
+  ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'),
+  ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five');
+
+SELECT * FROM temporal_partitioned;
+
+-- Update without moving within partition 1
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'one^1'
+  WHERE id = '[1,2)';
+
+-- Update without moving within partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'three^1'
+  WHERE id = '[3,4)';
+
+-- Update without moving within partition 5
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01'
+  SET name = 'five^1'
+  WHERE id = '[5,6)';
+
+-- Move from partition 1 to partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'one^2',
+      id = '[4,5)'
+  WHERE id = '[1,2)';
+
+-- Move from partition 3 to partition 1
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'three^2',
+      id = '[2,3)'
+  WHERE id = '[3,4)';
+
+-- Move from partition 5 to partition 3
+UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01'
+  SET name = 'five^2',
+      id = '[3,4)'
+  WHERE id = '[5,6)';
+
+-- Update all partitions at once (each with leftovers)
+
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at;
+SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at;
+SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
+
+DROP TABLE temporal_partitioned;
+
+RESET datestyle;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 66e06d91a41..fa527d2d53b 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -783,6 +783,33 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
 SET SESSION AUTHORIZATION regress_priv_user1;
 DROP TABLE errtst;
 
+-- test column-level privileges on the range used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+  c1 int4range,
+  valid_at tsrange,
+	CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+-- UPDATE requires select permission on the valid_at column (but not update):
+GRANT SELECT (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user3;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user1;
+-- DELETE requires select permission on the valid_at column:
+GRANT DELETE ON t1 TO regress_priv_user2;
+GRANT DELETE ON t1 TO regress_priv_user3;
+SET SESSION AUTHORIZATION regress_priv_user2;
+DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01';
+SET SESSION AUTHORIZATION regress_priv_user3;
+DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01';
+SET SESSION AUTHORIZATION regress_priv_user1;
+DROP TABLE t1;
+
 -- test column-level privileges when involved with DELETE
 SET SESSION AUTHORIZATION regress_priv_user1;
 ALTER TABLE atest6 ADD COLUMN three integer;
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index 1635adde2d4..f7646999bd4 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1889,6 +1889,20 @@ select * from uv_iocu_tab;
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
 
+-- Check UPDATE FOR PORTION OF works correctly
+create table uv_fpo_tab (id int4range, valid_at tsrange, b float,
+    constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps));
+insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0);
+create view uv_fpo_view as
+    select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab;
+
+insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1);
+select * from uv_fpo_view order by id, valid_at;
+update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]';
+select * from uv_fpo_view order by id, valid_at;
+delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]';
+select * from uv_fpo_view order by id, valid_at;
+
 -- Test whole-row references to the view
 create table uv_iocu_tab (a int unique, b text);
 create view uv_iocu_view as
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 77be6953575..b15679d675e 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -2,7 +2,7 @@
 --
 -- We leave behind several tables to test pg_dump etc:
 -- temporal_rng, temporal_rng2,
--- temporal_fk_rng2rng.
+-- temporal_fk_rng2rng, temporal_fk2_rng2rng.
 
 SET datestyle TO ISO, YMD;
 
@@ -632,6 +632,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
   ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'),
   ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar')
 ;
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01'
+  SET name = name || '1';
+UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01'
+  SET name = name || '2'
+  WHERE id = '[2,3)';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,2)', daterange('2005-01-01', '2006-01-01'), '[8,9)', 'foo3');
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[3,4)', daterange('2005-01-01', '2010-01-01'), '[9,10)', 'bar3');
 DROP TABLE temporal3;
 
 --
@@ -667,9 +681,23 @@ INSERT INTO temporal_partitioned (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'),
   ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three');
-SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
-SELECT * FROM tp1 ORDER BY id, valid_at;
-SELECT * FROM tp2 ORDER BY id, valid_at;
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,2)';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,5)'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,3)'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,4)';
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
 DROP TABLE temporal_partitioned;
 
 -- temporal UNIQUE:
@@ -685,9 +713,23 @@ INSERT INTO temporal_partitioned (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'),
   ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three');
-SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
-SELECT * FROM tp1 ORDER BY id, valid_at;
-SELECT * FROM tp2 ORDER BY id, valid_at;
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,2)';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,5)'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,3)'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,4)';
+SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at;
 DROP TABLE temporal_partitioned;
 
 -- ALTER TABLE REPLICA IDENTITY
@@ -1291,6 +1333,18 @@ COMMIT;
 -- changing the scalar part fails:
 UPDATE temporal_rng SET id = '[7,8)'
   WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- changing an unreferenced part is okay:
+UPDATE temporal_rng
+  FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+-- changing just a part fails:
+UPDATE temporal_rng
+  FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)';
 UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
@@ -1338,6 +1392,18 @@ BEGIN;
 
   DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
 COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+-- deleting just a part fails:
+DELETE FROM temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)';
 DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
@@ -1356,12 +1422,13 @@ ALTER TABLE temporal_fk_rng2rng
   ON DELETE RESTRICT;
 
 --
--- test ON UPDATE/DELETE options
+-- rng2rng test ON UPDATE/DELETE options
 --
 
 -- 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 ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)');
+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
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
@@ -1369,8 +1436,9 @@ ALTER TABLE temporal_fk_rng2rng
     ON DELETE CASCADE ON UPDATE CASCADE;
 
 -- test FK referenced updates SET NULL
-INSERT INTO temporal_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)');
+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
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     FOREIGN KEY (parent_id, PERIOD valid_at)
@@ -1378,9 +1446,10 @@ ALTER TABLE temporal_fk_rng2rng
     ON DELETE SET NULL ON UPDATE SET NULL;
 
 -- 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));
-INSERT INTO temporal_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)');
+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
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
   ADD CONSTRAINT temporal_fk_rng2rng_fk
@@ -1716,6 +1785,20 @@ BEGIN;
   WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 COMMIT;
 -- changing the scalar part fails:
+UPDATE temporal_mltrng SET id = '[7,8)'
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- changing an unreferenced part is okay:
+UPDATE temporal_mltrng
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+-- changing just a part fails:
+UPDATE temporal_mltrng
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+  SET id = '[7,8)'
+  WHERE id = '[5,6)';
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)';
 UPDATE temporal_mltrng SET id = '[7,8)'
   WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 
@@ -1760,6 +1843,17 @@ BEGIN;
 
   DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+WHERE id = '[5,6)';
+-- deleting just a part fails:
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+WHERE id = '[5,6)';
+-- 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'));
 
 --
 -- FK between partitioned tables: ranges
diff --git a/src/test/subscription/t/034_temporal.pl b/src/test/subscription/t/034_temporal.pl
index 66955e1b799..2fa376c24da 100644
--- a/src/test/subscription/t/034_temporal.pl
+++ b/src/test/subscription/t/034_temporal.pl
@@ -137,6 +137,7 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot update table "temporal_no_key" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't UPDATE temporal_no_key DEFAULT");
+# No need to test again with FOR PORTION OF
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
@@ -144,6 +145,12 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes
 HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't DELETE temporal_no_key DEFAULT");
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
+is( $stderr,
+	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes
+HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
+	"can't DELETE FOR PORTION OF temporal_no_key DEFAULT");
 
 $node_publisher->wait_for_catchup('sub1');
 
@@ -165,16 +172,22 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_pk ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk DEFAULT');
 
 # replicate with a unique key:
@@ -192,6 +205,7 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot update table "temporal_unique" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't UPDATE temporal_unique DEFAULT");
+# No need to test again with FOR PORTION OF
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
@@ -199,6 +213,12 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes
 HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't DELETE temporal_unique DEFAULT");
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
+is( $stderr,
+	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes
+HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
+	"can't DELETE FOR PORTION OF temporal_unique DEFAULT");
 
 $node_publisher->wait_for_catchup('sub1');
 
@@ -287,16 +307,22 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_no_key SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_no_key FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_no_key ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_no_key FULL');
 
 # replicate with a primary key:
@@ -310,16 +336,22 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_pk ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk FULL');
 
 # replicate with a unique key:
@@ -333,17 +365,23 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_unique FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_unique ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
-[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique FULL');
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique DEFAULT');
 
 # cleanup
 
@@ -425,16 +463,22 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_pk ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk USING INDEX');
 
 # replicate with a unique key:
@@ -448,16 +492,22 @@ $node_publisher->safe_psql(
 
 $node_publisher->safe_psql('postgres',
 	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_unique FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'");
 
 $node_publisher->safe_psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
 
 $node_publisher->wait_for_catchup('sub1');
 
 $result = $node_subscriber->safe_psql('postgres',
 	"SELECT * FROM temporal_unique ORDER BY id, valid_at");
 is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
-[2,3)|[2000-01-01,2010-01-01)|b
+[2,3)|[2000-01-01,2001-01-01)|b
+[2,3)|[2001-01-01,2002-01-01)|c
+[2,3)|[2003-01-01,2010-01-01)|b
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique USING INDEX');
 
 # cleanup
@@ -543,6 +593,7 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot update table "temporal_no_key" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't UPDATE temporal_no_key NOTHING");
+# No need to test again with FOR PORTION OF
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
@@ -550,6 +601,12 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes
 HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't DELETE temporal_no_key NOTHING");
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
+is( $stderr,
+	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes
+HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
+	"can't DELETE temporal_no_key NOTHING");
 
 $node_publisher->wait_for_catchup('sub1');
 
@@ -575,6 +632,7 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot update table "temporal_pk" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't UPDATE temporal_pk NOTHING");
+# No need to test again with FOR PORTION OF
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
@@ -582,6 +640,12 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_pk" because it does not have a replica identity and publishes deletes
 HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't DELETE temporal_pk NOTHING");
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
+is( $stderr,
+	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_pk" because it does not have a replica identity and publishes deletes
+HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
+	"can't DELETE temporal_pk NOTHING");
 
 $node_publisher->wait_for_catchup('sub1');
 
@@ -607,6 +671,7 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot update table "temporal_unique" because it does not have a replica identity and publishes updates
 HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't UPDATE temporal_unique NOTHING");
+# No need to test again with FOR PORTION OF
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
@@ -614,6 +679,12 @@ is( $stderr,
 	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes
 HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
 	"can't DELETE temporal_unique NOTHING");
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'");
+is( $stderr,
+	qq(psql:<stdin>:1: ERROR:  cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes
+HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.),
+	"can't DELETE FOR PORTION OF temporal_unique NOTHING");
 
 $node_publisher->wait_for_catchup('sub1');
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6e2d876a40f..b7c51f55067 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -854,6 +854,9 @@ ForBothState
 ForEachState
 ForFiveState
 ForFourState
+ForPortionOfClause
+ForPortionOfExpr
+ForPortionOfState
 ForThreeState
 ForeignAsyncConfigureWait_function
 ForeignAsyncNotify_function
-- 
2.47.3

From bba58ae1e6b770b8a9c7b9fd2b467288691805c4 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 16:11:47 -0700
Subject: [PATCH v66 5/7] Look up additional temporal foreign key helper proc

To implement CASCADE/SET NULL/SET DEFAULT on temporal foreign keys, we
need an intersect function. We can look them it when we look up the operators
already needed for temporal foreign keys (including NO ACTION constraints).

Author: Paul A. Jungwirth <[email-masked]>
---
 src/backend/catalog/pg_constraint.c | 32 ++++++++++++++++++++++++-----
 src/backend/commands/tablecmds.c    |  5 +++--
 src/backend/parser/analyze.c        |  2 +-
 src/backend/utils/adt/ri_triggers.c | 11 ++++++----
 src/include/catalog/pg_constraint.h |  9 ++++----
 5 files changed, 43 insertions(+), 16 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index b12765ae691..edb66a41fd6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1652,7 +1652,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 }
 
 /*
- * FindFKPeriodOpers -
+ * FindFKPeriodOpersAndProcs -
  *
  * Looks up the operator oids used for the PERIOD part of a temporal foreign key.
  * The opclass should be the opclass of that PERIOD element.
@@ -1663,12 +1663,15 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
  * That way foreign keys can compare fkattr <@ range_agg(pkattr).
  * intersectoperoid is used by NO ACTION constraints to trim the range being considered
  * to just what was updated/deleted.
+ * intersectprocoid is used to limit the effect of CASCADE/SET NULL/SET DEFAULT
+ * when the PK record is changed with FOR PORTION OF.
  */
 void
-FindFKPeriodOpers(Oid opclass,
-				  Oid *containedbyoperoid,
-				  Oid *aggedcontainedbyoperoid,
-				  Oid *intersectoperoid)
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *containedbyoperoid,
+						  Oid *aggedcontainedbyoperoid,
+						  Oid *intersectoperoid,
+						  Oid *intersectprocoid)
 {
 	Oid			opfamily = InvalidOid;
 	Oid			opcintype = InvalidOid;
@@ -1710,6 +1713,17 @@ FindFKPeriodOpers(Oid opclass,
 							   aggedcontainedbyoperoid,
 							   &strat);
 
+	/*
+	 * Hardcode intersect operators for ranges and multiranges, because we
+	 * don't have a better way to look up operators that aren't used in
+	 * indexes.
+	 *
+	 * If you change this code, you must change the code in
+	 * transformForPortionOfClause.
+	 *
+	 * XXX: Find a more extensible way to look up the operator, permitting
+	 * user-defined types.
+	 */
 	switch (opcintype)
 	{
 		case ANYRANGEOID:
@@ -1721,6 +1735,14 @@ FindFKPeriodOpers(Oid opclass,
 		default:
 			elog(ERROR, "unexpected opcintype: %u", opcintype);
 	}
+
+	/*
+	 * Look up the intersect proc. We use this in temporal foreign keys with
+	 * CASCADE/SET NULL/SET DEFAULT to build the FOR PORTION OF bounds. If
+	 * this is missing we don't need to complain here, because FOR PORTION OF
+	 * will not be allowed.
+	 */
+	*intersectprocoid = get_opcode(*intersectoperoid);
 }
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f976c0e5c7e..98ee5bab6cc 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10602,9 +10602,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			intersectprocoid;
 
-		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid,
-						  &intersectoperoid);
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid,
+								  &intersectoperoid, &intersectprocoid);
 	}
 
 	/* First, create the constraint catalog entry itself. */
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 56d422c36f5..a1fa43be7c8 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1548,7 +1548,7 @@ transformForPortionOfClause(ParseState *pstate,
 		/*
 		 * Whatever operator is used for intersect by temporal foreign keys,
 		 * we can use its backing procedure for intersects in FOR PORTION OF.
-		 * XXX: Share code with FindFKPeriodOpers?
+		 * XXX: Share code with FindFKPeriodOpersAndProcs?
 		 */
 		switch (opcintype)
 		{
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index bbadecef5f9..526afa49d2d 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -131,6 +131,8 @@ typedef struct RI_ConstraintInfo
 	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
 	Oid			period_intersect_oper;	/* anyrange * anyrange (or
 										 * multiranges) */
+	Oid			period_intersect_proc;	/* anyrange * anyrange (or
+										 * multiranges) */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -2339,10 +2341,11 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	{
 		Oid			opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);
 
-		FindFKPeriodOpers(opclass,
-						  &riinfo->period_contained_by_oper,
-						  &riinfo->agged_period_contained_by_oper,
-						  &riinfo->period_intersect_oper);
+		FindFKPeriodOpersAndProcs(opclass,
+								  &riinfo->period_contained_by_oper,
+								  &riinfo->agged_period_contained_by_oper,
+								  &riinfo->period_intersect_oper,
+								  &riinfo->period_intersect_proc);
 	}
 
 	ReleaseSysCache(tup);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index d5661b5bdff..caeaa816cf6 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -288,10 +288,11 @@ extern void DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 									   AttrNumber *conkey, AttrNumber *confkey,
 									   Oid *pf_eq_oprs, Oid *pp_eq_oprs, Oid *ff_eq_oprs,
 									   int *num_fk_del_set_cols, AttrNumber *fk_del_set_cols);
-extern void FindFKPeriodOpers(Oid opclass,
-							  Oid *containedbyoperoid,
-							  Oid *aggedcontainedbyoperoid,
-							  Oid *intersectoperoid);
+extern void FindFKPeriodOpersAndProcs(Oid opclass,
+									  Oid *containedbyoperoid,
+									  Oid *aggedcontainedbyoperoid,
+									  Oid *intersectoperoid,
+									  Oid *intersectprocoid);
 
 extern bool check_functional_grouping(Oid relid,
 									  Index varno, Index varlevelsup,
-- 
2.47.3

From 88760e977f07c4c2a5522b009e5a4e11abdd4b22 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v66 7/7] Expose FOR PORTION OF to plpgsql triggers

It is helpful for triggers to see what the FOR PORTION OF clause
specified: both the column/period name and the targeted bounds. Our RI
triggers require this information, and we are passing it as part of the
TriggerData struct. This commit allows plpgsql trigger functions to
access the same information, using the new TG_PERIOD_COLUMN and
TG_PERIOD_TARGET variables.

Author: Paul A. Jungwirth <[email-masked]>
---
 .../expected/level_tracking.out               |  2 +-
 doc/src/sgml/plpgsql.sgml                     | 24 ++++++++
 src/pl/plpgsql/src/pl_comp.c                  | 26 +++++++++
 src/pl/plpgsql/src/pl_exec.c                  | 32 +++++++++++
 src/pl/plpgsql/src/plpgsql.h                  |  2 +
 src/test/regress/expected/for_portion_of.out  | 55 ++++++++++---------
 src/test/regress/sql/for_portion_of.sql       |  9 ++-
 7 files changed, 122 insertions(+), 28 deletions(-)

diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out
index a15d897e59b..fae6b687751 100644
--- a/contrib/pg_stat_statements/expected/level_tracking.out
+++ b/contrib/pg_stat_statements/expected/level_tracking.out
@@ -1600,7 +1600,7 @@ SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements
   ORDER BY query COLLATE "C";
  toplevel | calls | rows | plans |                        query                        
 ----------+-------+------+-------+-----------------------------------------------------
- f        |     2 |    2 |     0 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id)
+ f        |     2 |    2 |     0 | INSERT INTO audit_table VALUES ($17, TG_OP, NEW.id)
  t        |     2 |    2 |     0 | INSERT INTO test_trigger VALUES ($1, $2)
  t        |     1 |    1 |     0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t
 (3 rows)
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 561f6e50d63..86f312416a5 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -4247,6 +4247,30 @@ ASSERT <replaceable class="parameter">condition</replaceable> <optional> , <repl
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry id="plpgsql-dml-trigger-tg-temporal-column">
+     <term><varname>TG_PERIOD_NAME</varname> <type>text</type></term>
+     <listitem>
+      <para>
+       the column name used in a <literal>FOR PORTION OF</literal> clause,
+       or else <symbol>NULL</symbol>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry id="plpgsql-dml-trigger-tg-temporal-target">
+     <term><varname>TG_PERIOD_BOUNDS</varname> <type>text</type></term>
+     <listitem>
+      <para>
+       the range/multirange/etc. given as the bounds of a
+       <literal>FOR PORTION OF</literal> clause, either directly (with parens syntax)
+       or computed from the <literal>FROM</literal> and <literal>TO</literal> bounds.
+       <symbol>NULL</symbol> if <literal>FOR PORTION OF</literal> was not used.
+       This is a text value based on the type's output function,
+       since the type can't be known at function creation time.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index 7d648c941c0..7e7ce20b85c 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -617,6 +617,32 @@ plpgsql_compile_callback(FunctionCallInfo fcinfo,
 			var->dtype = PLPGSQL_DTYPE_PROMISE;
 			((PLpgSQL_var *) var)->promise = PLPGSQL_PROMISE_TG_ARGV;
 
+			/* Add the variable tg_period_name */
+			var = plpgsql_build_variable("tg_period_name", 0,
+										 plpgsql_build_datatype(TEXTOID,
+																-1,
+																function->fn_input_collation,
+																NULL),
+										 true);
+			Assert(var->dtype == PLPGSQL_DTYPE_VAR);
+			var->dtype = PLPGSQL_DTYPE_PROMISE;
+			((PLpgSQL_var *) var)->promise = PLPGSQL_PROMISE_TG_PERIOD_NAME;
+
+			/*
+			 * Add the variable tg_period_bounds. This could be any rangetype
+			 * or multirangetype or user-supplied type, so the best we can
+			 * offer is a TEXT variable.
+			 */
+			var = plpgsql_build_variable("tg_period_bounds", 0,
+										 plpgsql_build_datatype(TEXTOID,
+																-1,
+																function->fn_input_collation,
+																NULL),
+										 true);
+			Assert(var->dtype == PLPGSQL_DTYPE_VAR);
+			var->dtype = PLPGSQL_DTYPE_PROMISE;
+			((PLpgSQL_var *) var)->promise = PLPGSQL_PROMISE_TG_PERIOD_BOUNDS;
+
 			break;
 
 		case PLPGSQL_EVENT_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 723048ab833..475e894317b 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -1384,6 +1384,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 						PLpgSQL_var *var)
 {
 	MemoryContext oldcontext;
+	ForPortionOfState *fpo;
 
 	if (var->promise == PLPGSQL_PROMISE_NONE)
 		return;					/* nothing to do */
@@ -1515,6 +1516,37 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 			}
 			break;
 
+		case PLPGSQL_PROMISE_TG_PERIOD_NAME:
+			if (estate->trigdata == NULL)
+				elog(ERROR, "trigger promise is not in a trigger function");
+			if (estate->trigdata->tg_temporal)
+				assign_text_var(estate, var, estate->trigdata->tg_temporal->fp_rangeName);
+			else
+				assign_simple_var(estate, var, (Datum) 0, true, false);
+			break;
+
+		case PLPGSQL_PROMISE_TG_PERIOD_BOUNDS:
+			if (estate->trigdata == NULL)
+				elog(ERROR, "trigger promise is not in a trigger function");
+
+			fpo = estate->trigdata->tg_temporal;
+			if (fpo)
+			{
+
+				Oid			funcid;
+				bool		varlena;
+
+				getTypeOutputInfo(fpo->fp_rangeType, &funcid, &varlena);
+				Assert(OidIsValid(funcid));
+
+				assign_text_var(estate, var,
+								OidOutputFunctionCall(funcid,
+													  fpo->fp_targetRange));
+			}
+			else
+				assign_simple_var(estate, var, (Datum) 0, true, false);
+			break;
+
 		case PLPGSQL_PROMISE_TG_EVENT:
 			if (estate->evtrigdata == NULL)
 				elog(ERROR, "event trigger promise is not in an event trigger function");
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index addb14a9959..70ffbb3b29a 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -85,6 +85,8 @@ typedef enum PLpgSQL_promise_type
 	PLPGSQL_PROMISE_TG_ARGV,
 	PLPGSQL_PROMISE_TG_EVENT,
 	PLPGSQL_PROMISE_TG_TAG,
+	PLPGSQL_PROMISE_TG_PERIOD_NAME,
+	PLPGSQL_PROMISE_TG_PERIOD_BOUNDS,
 } PLpgSQL_promise_type;
 
 /*
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 24caed16691..e774f38d478 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -1313,8 +1313,13 @@ CREATE FUNCTION dump_trigger()
 RETURNS TRIGGER LANGUAGE plpgsql AS
 $$
 BEGIN
-  RAISE NOTICE '%: % % %:',
-    TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+  IF TG_PERIOD_NAME IS NOT NULL THEN
+    RAISE NOTICE '%: % % FOR PORTION OF % (%) %:',
+      TG_NAME, TG_WHEN, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL;
+  ELSE
+    RAISE NOTICE '%: % % %:',
+      TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+  END IF;
 
   IF TG_ARGV[0] THEN
     RAISE NOTICE '  old: %', (SELECT string_agg(old_table::text, '\n       ') FROM old_table);
@@ -1364,10 +1369,10 @@ UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
   SET name = 'five^3'
   WHERE id = '[5,6)';
-NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:  fpo_before_stmt: BEFORE UPDATE FOR PORTION OF valid_at ([2021-01-01,2022-01-01)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:  fpo_before_row: BEFORE UPDATE FOR PORTION OF valid_at ([2021-01-01,2022-01-01)) ROW:
 NOTICE:    old: [2019-01-01,2030-01-01)
 NOTICE:    new: [2021-01-01,2022-01-01)
 NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
@@ -1394,19 +1399,19 @@ NOTICE:    new: [2022-01-01,2030-01-01)
 NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:  fpo_after_update_row: AFTER UPDATE FOR PORTION OF valid_at ([2021-01-01,2022-01-01)) ROW:
 NOTICE:    old: [2019-01-01,2030-01-01)
 NOTICE:    new: [2021-01-01,2022-01-01)
-NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE FOR PORTION OF valid_at ([2021-01-01,2022-01-01)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
 DELETE FROM for_portion_of_test
   FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
   WHERE id = '[5,6)';
-NOTICE:  fpo_before_stmt: BEFORE DELETE STATEMENT:
+NOTICE:  fpo_before_stmt: BEFORE DELETE FOR PORTION OF valid_at ([2023-01-01,2024-01-01)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_before_row: BEFORE DELETE ROW:
+NOTICE:  fpo_before_row: BEFORE DELETE FOR PORTION OF valid_at ([2023-01-01,2024-01-01)) ROW:
 NOTICE:    old: [2022-01-01,2030-01-01)
 NOTICE:    new: <NULL>
 NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
@@ -1433,10 +1438,10 @@ NOTICE:    new: [2024-01-01,2030-01-01)
 NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:  fpo_after_delete_row: AFTER DELETE FOR PORTION OF valid_at ([2023-01-01,2024-01-01)) ROW:
 NOTICE:    old: [2022-01-01,2030-01-01)
 NOTICE:    new: <NULL>
-NOTICE:  fpo_after_delete_stmt: AFTER DELETE STATEMENT:
+NOTICE:  fpo_after_delete_stmt: AFTER DELETE FOR PORTION OF valid_at ([2023-01-01,2024-01-01)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
 SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
@@ -1502,10 +1507,10 @@ BEGIN;
 UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
   SET name = '2018-01-15_to_2019-01-01';
-NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:  fpo_before_stmt: BEFORE UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:  fpo_before_row: BEFORE UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) ROW:
 NOTICE:    old: [2018-01-01,2020-01-01)
 NOTICE:    new: [2018-01-15,2019-01-01)
 NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
@@ -1532,20 +1537,20 @@ NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
 NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
-NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:  fpo_after_update_row: AFTER UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) ROW:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
-NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) STATEMENT:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
 ROLLBACK;
 BEGIN;
 DELETE FROM for_portion_of_test
   FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
-NOTICE:  fpo_before_stmt: BEFORE DELETE STATEMENT:
+NOTICE:  fpo_before_stmt: BEFORE DELETE FOR PORTION OF valid_at ((,2018-01-21)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_before_row: BEFORE DELETE ROW:
+NOTICE:  fpo_before_row: BEFORE DELETE FOR PORTION OF valid_at ((,2018-01-21)) ROW:
 NOTICE:    old: [2018-01-01,2020-01-01)
 NOTICE:    new: <NULL>
 NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
@@ -1560,10 +1565,10 @@ NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
 NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
-NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:  fpo_after_delete_row: AFTER DELETE FOR PORTION OF valid_at ((,2018-01-21)) ROW:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: <NULL>
-NOTICE:  fpo_after_delete_stmt: AFTER DELETE STATEMENT:
+NOTICE:  fpo_after_delete_stmt: AFTER DELETE FOR PORTION OF valid_at ((,2018-01-21)) STATEMENT:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: <NULL>
 ROLLBACK;
@@ -1571,10 +1576,10 @@ BEGIN;
 UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
   SET name = 'NULL_to_2018-01-01';
-NOTICE:  fpo_before_stmt: BEFORE UPDATE STATEMENT:
+NOTICE:  fpo_before_stmt: BEFORE UPDATE FOR PORTION OF valid_at ((,2018-01-02)) STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: <NULL>
-NOTICE:  fpo_before_row: BEFORE UPDATE ROW:
+NOTICE:  fpo_before_row: BEFORE UPDATE FOR PORTION OF valid_at ((,2018-01-02)) ROW:
 NOTICE:    old: [2018-01-01,2020-01-01)
 NOTICE:    new: [2018-01-01,2018-01-02)
 NOTICE:  fpo_before_stmt: BEFORE INSERT STATEMENT:
@@ -1589,10 +1594,10 @@ NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
 NOTICE:  fpo_after_insert_stmt: AFTER INSERT STATEMENT:
 NOTICE:    old: <NULL>
 NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
-NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:  fpo_after_update_row: AFTER UPDATE FOR PORTION OF valid_at ((,2018-01-02)) ROW:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
-NOTICE:  fpo_after_update_stmt: AFTER UPDATE STATEMENT:
+NOTICE:  fpo_after_update_stmt: AFTER UPDATE FOR PORTION OF valid_at ((,2018-01-02)) STATEMENT:
 NOTICE:    old: ("[1,2)","[2018-01-01,2020-01-01)",one)
 NOTICE:    new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
 ROLLBACK;
@@ -1629,7 +1634,7 @@ NOTICE:    new: [2018-01-01,2018-01-15)
 NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
 NOTICE:    old: <NULL>
 NOTICE:    new: [2019-01-01,2020-01-01)
-NOTICE:  fpo_after_update_row: AFTER UPDATE ROW:
+NOTICE:  fpo_after_update_row: AFTER UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) ROW:
 NOTICE:    old: [2018-01-01,2020-01-01)
 NOTICE:    new: [2018-01-15,2019-01-01)
 BEGIN;
@@ -1639,10 +1644,10 @@ COMMIT;
 NOTICE:  fpo_after_insert_row: AFTER INSERT ROW:
 NOTICE:    old: <NULL>
 NOTICE:    new: [2018-01-21,2019-01-01)
-NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:  fpo_after_delete_row: AFTER DELETE FOR PORTION OF valid_at ((,2018-01-21)) ROW:
 NOTICE:    old: [2018-01-15,2019-01-01)
 NOTICE:    new: <NULL>
-NOTICE:  fpo_after_delete_row: AFTER DELETE ROW:
+NOTICE:  fpo_after_delete_row: AFTER DELETE FOR PORTION OF valid_at ((,2018-01-21)) ROW:
 NOTICE:    old: [2018-01-01,2018-01-15)
 NOTICE:    new: <NULL>
 BEGIN;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index 72fb5273077..dbdfa3e98e3 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -873,8 +873,13 @@ CREATE FUNCTION dump_trigger()
 RETURNS TRIGGER LANGUAGE plpgsql AS
 $$
 BEGIN
-  RAISE NOTICE '%: % % %:',
-    TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+  IF TG_PERIOD_NAME IS NOT NULL THEN
+    RAISE NOTICE '%: % % FOR PORTION OF % (%) %:',
+      TG_NAME, TG_WHEN, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL;
+  ELSE
+    RAISE NOTICE '%: % % %:',
+      TG_NAME, TG_WHEN, TG_OP, TG_LEVEL;
+  END IF;
 
   IF TG_ARGV[0] THEN
     RAISE NOTICE '  old: %', (SELECT string_agg(old_table::text, '\n       ') FROM old_table);
-- 
2.47.3

From 400643a8a8b32ab00d7201103882d10621eb951e Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v66 6/7] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
 keys

Previously we raised an error for these options, because their
implementations require FOR PORTION OF. Now that we have temporal
UPDATE/DELETE, we can implement foreign keys that use it.

Author: Paul A. Jungwirth <[email-masked]>
---
 doc/src/sgml/ddl.sgml                         |    6 +-
 doc/src/sgml/ref/create_table.sgml            |   14 +-
 src/backend/commands/tablecmds.c              |   65 +-
 src/backend/utils/adt/ri_triggers.c           |  617 ++++++-
 src/include/catalog/pg_proc.dat               |   22 +
 src/test/regress/expected/btree_index.out     |   18 +-
 .../regress/expected/without_overlaps.out     | 1594 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     |  900 +++++++++-
 8 files changed, 3184 insertions(+), 52 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9070aaa5a7c..8582629dce8 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1848,9 +1848,9 @@ CREATE TABLE variants (
 
     <para>
      <productname>PostgreSQL</productname> supports temporal foreign keys with
-     action <literal>NO ACTION</literal>, but not <literal>RESTRICT</literal>,
-     <literal>CASCADE</literal>, <literal>SET NULL</literal>, or <literal>SET
-     DEFAULT</literal>.
+     action <literal>NO ACTION</literal>, <literal>CASCADE</literal>,
+     <literal>SET NULL</literal>, and <literal>SET DEFAULT</literal>, but not
+     <literal>RESTRICT</literal>.
     </para>
    </sect3>
   </sect2>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 77c5a763d45..fb04e18119c 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1315,7 +1315,9 @@ 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.
          </para>
         </listitem>
        </varlistentry>
@@ -1330,7 +1332,10 @@ 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 change will use <literal>FOR PORTION
+          OF</literal> semantics to constrain the effect to the bounds being
+          deleted/updated in the referenced row. The column maked with
+          <literal>PERIOD</literal> will not be set to null.
          </para>
         </listitem>
        </varlistentry>
@@ -1347,7 +1352,10 @@ 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 change will use <literal>FOR PORTION
+          OF</literal> semantics to constrain the effect to the bounds being
+          deleted/updated in the referenced row. The column marked with
+          <literal>PERIOD</literal> with not be set to a default value.
          </para>
         </listitem>
        </varlistentry>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 98ee5bab6cc..352ef726071 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -562,7 +562,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
 											   Relation rel, Constraint *fkconstraint,
 											   bool recurse, bool recursing,
 											   LOCKMODE lockmode);
-static int	validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+static int	validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, const int16 fkperiodattnum,
 										 int numfksetcols, int16 *fksetcolsattnums,
 										 List *fksetcols);
 static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
@@ -10112,6 +10112,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	int16		fkdelsetcols[INDEX_MAX_KEYS] = {0};
 	bool		with_period;
 	bool		pk_has_without_overlaps;
+	int16		fkperiodattnum = InvalidAttrNumber;
 	int			i;
 	int			numfks,
 				numpks,
@@ -10197,15 +10198,20 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid, fkcolloid);
 	with_period = fkconstraint->fk_with_period || fkconstraint->pk_with_period;
-	if (with_period && !fkconstraint->fk_with_period)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_FOREIGN_KEY),
-				errmsg("foreign key uses PERIOD on the referenced table but not the referencing table"));
+	if (with_period)
+	{
+		if (!fkconstraint->fk_with_period)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FOREIGN_KEY),
+					 errmsg("foreign key uses PERIOD on the referenced table but not the referencing table")));
+		fkperiodattnum = fkattnum[numfks - 1];
+	}
 
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
 											  fkdelsetcols, NULL, NULL);
 	numfkdelsetcols = validateFkOnDeleteSetColumns(numfks, fkattnum,
+												   fkperiodattnum,
 												   numfkdelsetcols,
 												   fkdelsetcols,
 												   fkconstraint->fk_del_set_cols);
@@ -10307,19 +10313,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	if (fkconstraint->fk_with_period)
 	{
-		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_RESTRICT ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT)
+		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_RESTRICT)
 			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_RESTRICT ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
+		if (fkconstraint->fk_del_action == FKCONSTR_ACTION_RESTRICT)
 			ereport(ERROR,
 					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					errmsg("unsupported %s action for foreign key constraint using PERIOD",
@@ -10675,6 +10675,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 static int
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10688,6 +10689,14 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 		/* Make sure it's in fkattnums[] */
 		for (int j = 0; j < numfks; j++)
 		{
+			if (fkperiodattnum == setcol_attnum)
+			{
+				char	   *col = strVal(list_nth(fksetcols, i));
+
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+						 errmsg("column \"%s\" referenced in ON DELETE SET action cannot be PERIOD", col)));
+			}
 			if (fkattnums[j] == setcol_attnum)
 			{
 				seen = true;
@@ -13926,17 +13935,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 		case FKCONSTR_ACTION_CASCADE:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_cascade_del");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
 			break;
 		case FKCONSTR_ACTION_SETNULL:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setnull_del");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
 			break;
 		case FKCONSTR_ACTION_SETDEFAULT:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setdefault_del");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
 			break;
 		default:
 			elog(ERROR, "unrecognized FK action type: %d",
@@ -13986,17 +14004,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 		case FKCONSTR_ACTION_CASCADE:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_cascade_upd");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
 			break;
 		case FKCONSTR_ACTION_SETNULL:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setnull_upd");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
 			break;
 		case FKCONSTR_ACTION_SETDEFAULT:
 			fk_trigger->deferrable = false;
 			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
+			if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setdefault_upd");
+			else
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
 			break;
 		default:
 			elog(ERROR, "unrecognized FK action type: %d",
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 526afa49d2d..5483406b4fe 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -79,6 +79,12 @@
 #define RI_PLAN_SETNULL_ONUPDATE		8
 #define RI_PLAN_SETDEFAULT_ONDELETE		9
 #define RI_PLAN_SETDEFAULT_ONUPDATE		10
+#define RI_PLAN_PERIOD_CASCADE_ONDELETE		11
+#define RI_PLAN_PERIOD_CASCADE_ONUPDATE		12
+#define RI_PLAN_PERIOD_SETNULL_ONUPDATE		13
+#define RI_PLAN_PERIOD_SETNULL_ONDELETE		14
+#define RI_PLAN_PERIOD_SETDEFAULT_ONUPDATE	15
+#define RI_PLAN_PERIOD_SETDEFAULT_ONDELETE	16
 
 #define MAX_QUOTED_NAME_LEN  (NAMEDATALEN*2+3)
 #define MAX_QUOTED_REL_NAME_LEN  (MAX_QUOTED_NAME_LEN*2)
@@ -196,6 +202,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 							  const RI_ConstraintInfo *riinfo);
 static Datum ri_restrict(TriggerData *trigdata, bool is_no_action);
 static Datum ri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
+static Datum tri_set(TriggerData *trigdata, bool is_set_null, int tgkind);
 static void quoteOneName(char *buffer, const char *name);
 static void quoteRelationName(char *buffer, Relation rel);
 static void ri_GenerateQual(StringInfo buf,
@@ -232,6 +239,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 							RI_QueryKey *qkey, SPIPlanPtr qplan,
 							Relation fk_rel, Relation pk_rel,
 							TupleTableSlot *oldslot, TupleTableSlot *newslot,
+							int periodParam, Datum period,
 							bool is_restrict,
 							bool detectNewRows, int expect_OK);
 static void ri_ExtractValues(Relation rel, TupleTableSlot *slot,
@@ -241,6 +249,11 @@ pg_noreturn static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 										   Relation pk_rel, Relation fk_rel,
 										   TupleTableSlot *violatorslot, TupleDesc tupdesc,
 										   int queryno, bool is_restrict, bool partgone);
+static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal,
+								 const RI_ConstraintInfo *riinfo);
+static Datum restrict_enforced_range(const ForPortionOfState *tg_temporal,
+									 const RI_ConstraintInfo *riinfo,
+									 TupleTableSlot *oldslot);

 /*
@@ -454,6 +467,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					false,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
@@ -619,6 +633,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 	result = ri_PerformCheck(riinfo, &qkey, qplan,
 							 fk_rel, pk_rel,
 							 oldslot, NULL,
+							 -1, (Datum) 0,
 							 false,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
@@ -895,6 +910,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					!is_no_action,
 					true,		/* must detect new rows */
 					SPI_OK_SELECT);
@@ -997,6 +1013,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
@@ -1114,6 +1131,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
@@ -1342,6 +1360,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
@@ -1373,6 +1392,540 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
+ * RI_FKey_period_cascade_del -
+ *
+ * Cascaded delete foreign key references at delete event on temporal PK table.
+ */
+Datum
+RI_FKey_period_cascade_del(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const RI_ConstraintInfo *riinfo;
+	Relation	fk_rel;
+	Relation	pk_rel;
+	TupleTableSlot *oldslot;
+	RI_QueryKey qkey;
+	SPIPlanPtr	qplan;
+	Datum		targetRange;
+
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_cascade_del", RI_TRIGTYPE_DELETE);
+
+	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+									trigdata->tg_relation, true);
+
+	/*
+	 * Get the relation descriptors of the FK and PK tables and the old tuple.
+	 *
+	 * fk_rel is opened in RowExclusiveLock mode since that's what our
+	 * eventual DELETE will get on it.
+	 */
+	fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+	pk_rel = trigdata->tg_relation;
+	oldslot = trigdata->tg_trigslot;
+
+	/*
+	 * Don't delete than more than the PK's duration, trimmed by an original
+	 * FOR PORTION OF if necessary.
+	 */
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI_connect failed");
+
+	/* Fetch or prepare a saved plan for the cascaded delete */
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_PERIOD_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			pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+			Oid			fk_coll = RIAttCollation(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);
+			if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+				ri_GenerateQualCollation(&querybuf, pk_coll);
+			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);
+
+	if (SPI_finish() != SPI_OK_FINISH)
+		elog(ERROR, "SPI_finish failed");
+
+	table_close(fk_rel, RowExclusiveLock);
+
+	return PointerGetDatum(NULL);
+}
+
+/*
+ * RI_FKey_period_cascade_upd -
+ *
+ * Cascaded update foreign key references at update event on temporal PK table.
+ */
+Datum
+RI_FKey_period_cascade_upd(PG_FUNCTION_ARGS)
+{
+	TriggerData *trigdata = (TriggerData *) fcinfo->context;
+	const RI_ConstraintInfo *riinfo;
+	Relation	fk_rel;
+	Relation	pk_rel;
+	TupleTableSlot *oldslot;
+	TupleTableSlot *newslot;
+	RI_QueryKey qkey;
+	SPIPlanPtr	qplan;
+	Datum		targetRange;
+
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_cascade_upd", RI_TRIGTYPE_UPDATE);
+
+	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+									trigdata->tg_relation, true);
+
+	/*
+	 * Get the relation descriptors of the FK and PK tables and the new and
+	 * old tuple.
+	 *
+	 * fk_rel is opened in RowExclusiveLock mode since that's what our
+	 * eventual UPDATE will get on it.
+	 */
+	fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+	pk_rel = trigdata->tg_relation;
+	newslot = trigdata->tg_newslot;
+	oldslot = trigdata->tg_trigslot;
+
+	/*
+	 * Don't delete than more than the PK's duration, trimmed by an original
+	 * FOR PORTION OF if necessary.
+	 */
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI_connect failed");
+
+	/* Fetch or prepare a saved plan for the cascaded update */
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_PERIOD_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			pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+			Oid			fk_coll = RIAttCollation(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);
+			if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+				ri_GenerateQualCollation(&querybuf, pk_coll);
+			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);
+
+	if (SPI_finish() != SPI_OK_FINISH)
+		elog(ERROR, "SPI_finish failed");
+
+	table_close(fk_rel, RowExclusiveLock);
+
+	return PointerGetDatum(NULL);
+}
+
+/*
+ * RI_FKey_period_setnull_del -
+ *
+ * Set foreign key references to NULL values at delete event on PK table.
+ */
+Datum
+RI_FKey_period_setnull_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setnull_del", RI_TRIGTYPE_DELETE);
+
+	/* Share code with UPDATE case */
+	return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * RI_FKey_period_setnull_upd -
+ *
+ * Set foreign key references to NULL at update event on PK table.
+ */
+Datum
+RI_FKey_period_setnull_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setnull_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with DELETE case */
+	return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * RI_FKey_period_setdefault_del -
+ *
+ * Set foreign key references to defaults at delete event on PK table.
+ */
+Datum
+RI_FKey_period_setdefault_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setdefault_del", RI_TRIGTYPE_DELETE);
+
+	/* Share code with UPDATE case */
+	return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * RI_FKey_period_setdefault_upd -
+ *
+ * Set foreign key references to defaults at update event on PK table.
+ */
+Datum
+RI_FKey_period_setdefault_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setdefault_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with DELETE case */
+	return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * tri_set -
+ *
+ * Common code for temporal ON DELETE SET NULL, ON DELETE SET DEFAULT, ON
+ * UPDATE SET NULL, and ON UPDATE SET DEFAULT.
+ */
+static Datum
+tri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
+{
+	const RI_ConstraintInfo *riinfo;
+	Relation	fk_rel;
+	Relation	pk_rel;
+	TupleTableSlot *oldslot;
+	RI_QueryKey qkey;
+	SPIPlanPtr	qplan;
+	Datum		targetRange;
+	int32		queryno;
+
+	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
+									trigdata->tg_relation, true);
+
+	/*
+	 * Get the relation descriptors of the FK and PK tables and the old tuple.
+	 *
+	 * fk_rel is opened in RowExclusiveLock mode since that's what our
+	 * eventual UPDATE will get on it.
+	 */
+	fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock);
+	pk_rel = trigdata->tg_relation;
+	oldslot = trigdata->tg_trigslot;
+
+	/*
+	 * Don't SET NULL/DEFAULT more than the PK's duration, trimmed by an
+	 * original FOR PORTION OF if necessary.
+	 */
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+
+	if (SPI_connect() != SPI_OK_CONNECT)
+		elog(ERROR, "SPI_connect failed");
+
+	/*
+	 * Fetch or prepare a saved plan for the trigger.
+	 */
+	switch (tgkind)
+	{
+		case RI_TRIGTYPE_UPDATE:
+			queryno = is_set_null
+				? RI_PLAN_PERIOD_SETNULL_ONUPDATE
+				: RI_PLAN_PERIOD_SETDEFAULT_ONUPDATE;
+			break;
+		case RI_TRIGTYPE_DELETE:
+			queryno = is_set_null
+				? RI_PLAN_PERIOD_SETNULL_ONDELETE
+				: RI_PLAN_PERIOD_SETDEFAULT_ONDELETE;
+			break;
+		default:
+			elog(ERROR, "invalid tgkind passed to ri_set");
+	}
+
+	ri_BuildQueryKey(&qkey, riinfo, queryno);
+
+	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[RI_MAX_NUMKEYS + 1];	/* +1 for FOR PORTION OF */
+		const char *fk_only;
+		int			num_cols_to_set;
+		const int16 *set_cols;
+
+		switch (tgkind)
+		{
+			case RI_TRIGTYPE_UPDATE:
+				/* -1 so we let FOR PORTION OF set the range. */
+				num_cols_to_set = riinfo->nkeys - 1;
+				set_cols = riinfo->fk_attnums;
+				break;
+			case RI_TRIGTYPE_DELETE:
+
+				/*
+				 * If confdelsetcols are present, then we only update the
+				 * columns specified in that array, otherwise we update all
+				 * the referencing columns.
+				 */
+				if (riinfo->ndelsetcols != 0)
+				{
+					num_cols_to_set = riinfo->ndelsetcols;
+					set_cols = riinfo->confdelsetcols;
+				}
+				else
+				{
+					/* -1 so we let FOR PORTION OF set the range. */
+					num_cols_to_set = riinfo->nkeys - 1;
+					set_cols = riinfo->fk_attnums;
+				}
+				break;
+			default:
+				elog(ERROR, "invalid tgkind passed to ri_set");
+		}
+
+		/* ----------
+		 * The query string built is
+		 *	UPDATE [ONLY] <fktable>
+		 *			FOR PORTION OF $fkatt (${n+1})
+		 *			SET fkatt1 = {NULL|DEFAULT} [, ...]
+		 *			WHERE $1 = fkatt1 [AND ...]
+		 * The type id's for the $ parameters are those of the
+		 * corresponding PK attributes.
+		 * ----------
+		 */
+		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, riinfo->nkeys + 1);
+
+		/*
+		 * Add assignment clauses
+		 */
+		querysep = "";
+		for (int i = 0; i < num_cols_to_set; i++)
+		{
+			quoteOneName(attname, RIAttName(fk_rel, set_cols[i]));
+			appendStringInfo(&querybuf,
+							 "%s %s = %s",
+							 querysep, attname,
+							 is_set_null ? "NULL" : "DEFAULT");
+			querysep = ",";
+		}
+
+		/*
+		 * Add WHERE clause
+		 */
+		qualsep = "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]);
+			Oid			pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+			Oid			fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+
+			quoteOneName(attname,
+						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
+			sprintf(paramname, "$%d", i + 1);
+			ri_GenerateQual(&querybuf, qualsep,
+							paramname, pk_type,
+							riinfo->pf_eq_oprs[i],
+							attname, fk_type);
+			if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll))
+				ri_GenerateQualCollation(&querybuf, pk_coll);
+			qualsep = "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. Run it to update the existing references.
+	 */
+	ri_PerformCheck(riinfo, &qkey, qplan,
+					fk_rel, pk_rel,
+					oldslot, NULL,
+					riinfo->nkeys + 1, targetRange,
+					false,
+					true,		/* must detect new rows */
+					SPI_OK_UPDATE);
+
+	if (SPI_finish() != SPI_OK_FINISH)
+		elog(ERROR, "SPI_finish failed");
+
+	table_close(fk_rel, RowExclusiveLock);
+
+	if (is_set_null)
+		return PointerGetDatum(NULL);
+	else
+	{
+		/*
+		 * If we just deleted or updated the PK row whose key was equal to the
+		 * FK columns' default values, and a referencing row exists in the FK
+		 * table, we would have updated that row to the same values it already
+		 * had --- and RI_FKey_fk_upd_check_required would hence believe no
+		 * check is necessary.  So we need to do another lookup now and in
+		 * case a reference still exists, abort the operation.  That is
+		 * already implemented in the NO ACTION trigger, so just run it. (This
+		 * recheck is only needed in the SET DEFAULT case, since CASCADE would
+		 * remove such rows in case of a DELETE operation or would change the
+		 * FK key values in case of an UPDATE, while SET NULL is certain to
+		 * result in rows that satisfy the FK constraint.)
+		 */
+		return ri_restrict(trigdata, true);
+	}
+}
+
 /*
  * RI_FKey_pk_upd_check_required -
  *
@@ -2488,6 +3041,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 				RI_QueryKey *qkey, SPIPlanPtr qplan,
 				Relation fk_rel, Relation pk_rel,
 				TupleTableSlot *oldslot, TupleTableSlot *newslot,
+				int periodParam, Datum period,
 				bool is_restrict,
 				bool detectNewRows, int expect_OK)
 {
@@ -2500,8 +3054,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
-	Datum		vals[RI_MAX_NUMKEYS * 2];
-	char		nulls[RI_MAX_NUMKEYS * 2];
+	Datum		vals[RI_MAX_NUMKEYS * 2 + 1];
+	char		nulls[RI_MAX_NUMKEYS * 2 + 1];
 
 	/*
 	 * Use the query type code to determine whether the query is run against
@@ -2544,6 +3098,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 		ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
 						 vals, nulls);
 	}
+	/* Add/replace a query param for the PERIOD if needed */
+	if (period)
+	{
+		vals[periodParam - 1] = period;
+		nulls[periodParam - 1] = ' ';
+	}
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3224,6 +3784,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 		case F_RI_FKEY_SETDEFAULT_UPD:
 		case F_RI_FKEY_NOACTION_DEL:
 		case F_RI_FKEY_NOACTION_UPD:
+		case F_RI_FKEY_PERIOD_CASCADE_DEL:
+		case F_RI_FKEY_PERIOD_CASCADE_UPD:
+		case F_RI_FKEY_PERIOD_SETNULL_DEL:
+		case F_RI_FKEY_PERIOD_SETNULL_UPD:
+		case F_RI_FKEY_PERIOD_SETDEFAULT_DEL:
+		case F_RI_FKEY_PERIOD_SETDEFAULT_UPD:
 			return RI_TRIGGER_PK;
 
 		case F_RI_FKEY_CHECK_INS:
@@ -3233,3 +3799,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
+ * Returns true iff the primary key referenced by riinfo includes the range
+ * column targeted by the FOR PORTION OF clause (according to tg_temporal).
+ */
+static bool
+fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo)
+{
+	if (tg_temporal == NULL)
+		return false;
+
+	return riinfo->pk_attnums[riinfo->nkeys - 1] == tg_temporal->fp_rangeAttno;
+}
+
+/*
+ * restrict_enforced_range -
+ *
+ * Returns a Datum of RangeTypeP holding the appropriate timespan
+ * to target child records when we RESTRICT/CASCADE/SET NULL/SET DEFAULT.
+ *
+ * In a normal UPDATE/DELETE this should be the referenced row's own valid time,
+ * but if there was a FOR PORTION OF clause, then we should use that to
+ * trim down the span further.
+ */
+static Datum
+restrict_enforced_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+{
+	Datum		pkRecordRange;
+	bool		isnull;
+	AttrNumber	attno = riinfo->pk_attnums[riinfo->nkeys - 1];
+
+	pkRecordRange = slot_getattr(oldslot, attno, &isnull);
+	if (isnull)
+		elog(ERROR, "application time should not be null");
+
+	if (fpo_targets_pk_range(tg_temporal, riinfo))
+	{
+		if (!OidIsValid(riinfo->period_intersect_proc))
+			elog(ERROR, "invalid intersect support function");
+
+		return OidFunctionCall2(riinfo->period_intersect_proc, pkRecordRange, tg_temporal->fp_targetRange);
+	}
+	else
+		return pkRecordRange;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..fc22f31ea07 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4129,6 +4129,28 @@
   prorettype => 'trigger', proargtypes => '',
   prosrc => 'RI_FKey_noaction_upd' },
 
+# Temporal referential integrity constraint triggers
+{ oid => '6124', descr => 'temporal referential integrity ON DELETE CASCADE',
+  proname => 'RI_FKey_period_cascade_del', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_cascade_del' },
+{ oid => '6125', descr => 'temporal referential integrity ON UPDATE CASCADE',
+  proname => 'RI_FKey_period_cascade_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_cascade_upd' },
+{ oid => '6128', descr => 'temporal referential integrity ON DELETE SET NULL',
+  proname => 'RI_FKey_period_setnull_del', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_setnull_del' },
+{ oid => '6129', descr => 'temporal referential integrity ON UPDATE SET NULL',
+  proname => 'RI_FKey_period_setnull_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_setnull_upd' },
+{ oid => '6130', descr => 'temporal referential integrity ON DELETE SET DEFAULT',
+  proname => 'RI_FKey_period_setdefault_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_setdefault_del' },
+{ oid => '6131', descr => 'temporal referential integrity ON UPDATE SET DEFAULT',
+  proname => 'RI_FKey_period_setdefault_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_setdefault_upd' },
+
 { oid => '1666',
   proname => 'varbiteq', proleakproof => 't', prorettype => 'bool',
   proargtypes => 'varbit varbit', prosrc => 'biteq' },
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 21dc9b5783a..c3bf94797e7 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -454,14 +454,17 @@ select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
 (3 rows)
 
 select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
-        proname         
-------------------------
+            proname            
+-------------------------------
  RI_FKey_cascade_del
  RI_FKey_noaction_del
+ RI_FKey_period_cascade_del
+ RI_FKey_period_setdefault_del
+ RI_FKey_period_setnull_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(5 rows)
+(8 rows)
 
 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
@@ -500,14 +503,17 @@ select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
 (6 rows)
 
 select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
-        proname         
-------------------------
+            proname            
+-------------------------------
  RI_FKey_cascade_del
  RI_FKey_noaction_del
+ RI_FKey_period_cascade_del
+ RI_FKey_period_setdefault_del
+ RI_FKey_period_setnull_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(5 rows)
+(8 rows)
 
 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 73b2c78a4ce..4b123c6a8bb 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -1947,7 +1947,24 @@ ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
 --
 -- 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)
+-- referenced updates SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols, SET NULL subset)
+-- referenced updates SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
 -- 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)');
@@ -1956,29 +1973,593 @@ 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:
+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)
+
+-- 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:  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) | 
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(3 rows)
+
+-- non-FPO update:
+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) | 
+ [100,101) | [2019-01-01,2020-01-01) | 
+ [100,101) | [2020-01-01,2021-01-01) | 
+(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) | 
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes 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)');
+-- 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) | [2019-01-01,2020-01-01) | 
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(3 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 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2019-01-01) | 
+ [100,101) | [2019-01-01,2020-01-01) | 
+ [100,101) | [2020-01-01,2021-01-01) | 
+(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)');
+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) | [2018-01-01,2020-01-01) | 
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+--
 -- 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));
 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
   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:  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) | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(3 rows)
+
+-- non-FPO update:
+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) | [-1,0)
+ [100,101) | [2019-01-01,2020-01-01) | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [-1,0)
+(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) | [-1,0)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null));
+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) | [2019-01-01,2020-01-01) | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)
+(3 rows)
+
+-- non-FPO update:
+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 
+-----------+-------------------------+-----------
+ [100,101) | [2018-01-01,2019-01-01) | [-1,0)
+ [100,101) | [2019-01-01,2020-01-01) | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [-1,0)
+(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)');
+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) | [2018-01-01,2020-01-01) | [-1,0)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)
+(2 rows)
+
+--
+-- 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 FK referenced updates SET NULL (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 SET NULL ON UPDATE SET NULL;
+-- 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) |            | 
+ [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) |            | 
+ [100,101) | [2019-01-01,2020-01-01) |            | 
+ [100,101) | [2020-01-01,2021-01-01) |            | 
+(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) |            | 
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET NULL (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) | [2019-01-01,2020-01-01) |            | 
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 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 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) |            | 
+ [100,101) | [2019-01-01,2020-01-01) |            | 
+ [100,101) | [2020-01-01,2021-01-01) |            | 
+(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)');
+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) | [2018-01-01,2020-01-01) |            | 
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+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)');
+-- fails because you can't set the PERIOD column:
+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 SET NULL (valid_at) ON UPDATE SET NULL;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
+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 SET NULL (parent_id1) ON UPDATE SET NULL;
+-- 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) | [2019-01-01,2020-01-01) |            | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 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 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) |            | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) |            | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) |            | [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)');
+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) | [2018-01-01,2020-01-01) |            | [8,9)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', daterange(null, null));
+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
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)', id2 = '[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) | [-1,0)     | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,8)', id2 = '[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) | [-1,0)     | [-1,0)
+ [100,101) | [2019-01-01,2020-01-01) | [-1,0)     | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [-1,0)     | [-1,0)
+(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)', id2 = '[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) | [-1,0)     | [-1,0)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', daterange(null, null));
+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) | [2019-01-01,2020-01-01) | [-1,0)     | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+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 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) | [-1,0)     | [-1,0)
+ [100,101) | [2019-01-01,2020-01-01) | [-1,0)     | [-1,0)
+ [100,101) | [2020-01-01,2021-01-01) | [-1,0)     | [-1,0)
+(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)');
+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) | [2018-01-01,2020-01-01) | [-1,0)     | [-1,0)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', daterange(null, null));
+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)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
+ALTER TABLE temporal_fk2_rng2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- 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) | [2019-01-01,2020-01-01) | [-1,0)     | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+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 
+-----------+-------------------------+------------+------------
+ [100,101) | [2018-01-01,2019-01-01) | [-1,0)     | [6,7)
+ [100,101) | [2019-01-01,2020-01-01) | [-1,0)     | [6,7)
+ [100,101) | [2020-01-01,2021-01-01) | [-1,0)     | [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_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', daterange(null, null));
+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) | [2018-01-01,2020-01-01) | [-1,0)     | [8,9)
+ [200,201) | [2020-01-01,2021-01-01) | [8,9)      | [8,9)
+(2 rows)
+
 --
 -- test FOREIGN KEY, multirange references multirange
 --
@@ -2413,6 +2994,626 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02
 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)
+-- referenced updates SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols, SET NULL subset)
+-- referenced updates SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+-- 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:
+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)
+
+-- 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 SET NULL
+--
+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 SET NULL ON UPDATE SET NULL;
+-- 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)}                         | 
+(2 rows)
+
+-- non-FPO update:
+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)} | 
+ [100,101) | {[2019-01-01,2020-01-01)}                         | 
+(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)} | 
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET NULL
+--
+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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | 
+(2 rows)
+
+-- 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 
+-----------+---------------------------------------------------+-----------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | 
+ [100,101) | {[2019-01-01,2020-01-01)}                         | 
+(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)');
+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) | {[2018-01-01,2020-01-01)} | 
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(2 rows)
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[-1,-1]', datemultirange(daterange(null, null)));
+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
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_mltrng
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- 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)}                         | [-1,0)
+(2 rows)
+
+-- non-FPO update:
+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)} | [-1,0)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)
+(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)} | [-1,0)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[-1,-1]', datemultirange(daterange(null, null)));
+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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)
+(2 rows)
+
+-- non-FPO update:
+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 
+-----------+---------------------------------------------------+-----------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [-1,0)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)
+(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)');
+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) | {[2018-01-01,2020-01-01)} | [-1,0)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)
+(2 rows)
+
+--
+-- 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)
+
+--
+-- test FK referenced updates SET NULL (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 SET NULL ON UPDATE SET NULL;
+-- 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)}                         |            | 
+(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)} |            | 
+ [100,101) | {[2019-01-01,2020-01-01)}                         |            | 
+(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)} |            | 
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET NULL (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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         |            | 
+(2 rows)
+
+-- 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 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} |            | 
+ [100,101) | {[2019-01-01,2020-01-01)}                         |            | 
+(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)');
+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) | {[2018-01-01,2020-01-01)} |            | 
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+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)');
+-- fails because you can't set the PERIOD column:
+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 SET NULL (valid_at) ON UPDATE SET NULL;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
+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 SET NULL (parent_id1) ON UPDATE SET NULL;
+-- 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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         |            | [6,7)
+(2 rows)
+
+-- 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 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} |            | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)}                         |            | [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)');
+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) | {[2018-01-01,2020-01-01)} |            | [8,9)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', datemultirange(daterange(null, null)));
+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
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id1 = '[7,8)', id2 = '[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)}                         | [-1,0)     | [-1,0)
+(2 rows)
+
+-- non-FPO update:
+UPDATE temporal_mltrng2 SET id1 = '[7,8)', id2 = '[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)} | [-1,0)     | [-1,0)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)     | [-1,0)
+(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)', id2 = '[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)} | [-1,0)     | [-1,0)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', datemultirange(daterange(null, null)));
+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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)     | [-1,0)
+(2 rows)
+
+-- non-FPO update:
+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 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [-1,0)     | [-1,0)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)     | [-1,0)
+(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)');
+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) | {[2018-01-01,2020-01-01)} | [-1,0)     | [-1,0)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', datemultirange(daterange(null, null)));
+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)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- 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)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)     | [6,7)
+(2 rows)
+
+-- non-FPO update:
+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 
+-----------+---------------------------------------------------+------------+------------
+ [100,101) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [-1,0)     | [6,7)
+ [100,101) | {[2019-01-01,2020-01-01)}                         | [-1,0)     | [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_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', datemultirange(daterange(null, null)));
+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) | {[2018-01-01,2020-01-01)} | [-1,0)     | [8,9)
+ [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
+(2 rows)
+
+-- FK with a custom range type
+CREATE TYPE mydaterange AS range(subtype=date);
+CREATE TABLE temporal_rng3 (
+  id int4range,
+  valid_at mydaterange,
+  CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk3_rng2rng (
+  id int4range,
+  valid_at mydaterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk3_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk3_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng3 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[8,9)', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk3_rng2rng (id, valid_at, parent_id) VALUES ('[5,6)', mydaterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_rng3 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,9)';
+SELECT * FROM temporal_fk3_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)
+
+DROP TABLE temporal_fk3_rng2rng;
+DROP TABLE temporal_rng3;
+DROP TYPE mydaterange;
+--
 -- FK between partitioned tables: ranges
 --
 CREATE TABLE temporal_partitioned_rng (
@@ -2421,8 +3622,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'),
@@ -2435,8 +3636,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
 --
@@ -2478,7 +3679,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
@@ -2490,37 +3691,162 @@ 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
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)');
 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 SET NULL ON UPDATE SET NULL;
-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 = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,7)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [6,7) | [2019-01-01,2020-01-01) | 
+ [6,7) | [2018-01-01,2019-01-01) | [9,10)
+ [6,7) | [2020-01-01,2021-01-01) | [9,10)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,7)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [6,7) | [2019-01-01,2020-01-01) | 
+ [6,7) | [2018-01-01,2019-01-01) | 
+ [6,7) | [2020-01-01,2021-01-01) | 
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[18,19)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[18,19)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'), '[18,19)');
+UPDATE temporal_partitioned_rng SET id = '[19,20)' WHERE id = '[18,19)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,13)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [12,13) | [2018-01-01,2020-01-01) | 
+ [12,13) | [2020-01-01,2021-01-01) | [18,19)
+(2 rows)
+
 --
 -- partitioned FK referenced deletes SET NULL
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[11,12)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[7,8)', daterange('2018-01-01', '2021-01-01'), '[11,12)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,8)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [7,8) | [2019-01-01,2020-01-01) | 
+ [7,8) | [2018-01-01,2019-01-01) | [11,12)
+ [7,8) | [2020-01-01,2021-01-01) | [11,12)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,8)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [7,8) | [2019-01-01,2020-01-01) | 
+ [7,8) | [2018-01-01,2019-01-01) | 
+ [7,8) | [2020-01-01,2021-01-01) | 
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[20,21)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[20,21)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[13,14)', daterange('2018-01-01', '2021-01-01'), '[20,21)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,21)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,14)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [13,14) | [2018-01-01,2020-01-01) | 
+ [13,14) | [2020-01-01,2021-01-01) | [20,21)
+(2 rows)
+
 --
 -- partitioned FK referenced updates SET DEFAULT
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[0,1)', daterange(null, null));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)');
 ALTER TABLE temporal_partitioned_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
   DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
@@ -2528,10 +3854,73 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-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 = '[13,14)' WHERE id = '[12,13)';
+ERROR:  insert or update on table "tfkp2" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2019-01-01,2020-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,9)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [8,9) | [2018-01-01,2021-01-01) | [12,13)
+(1 row)
+
+UPDATE temporal_partitioned_rng SET id = '[13,14)' WHERE id = '[12,13)';
+ERROR:  insert or update on table "tfkp2" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2018-01-01,2021-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,9)';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [8,9) | [2018-01-01,2021-01-01) | [12,13)
+(1 row)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[22,23)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[22,23)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[14,15)', daterange('2018-01-01', '2021-01-01'), '[22,23)');
+UPDATE temporal_partitioned_rng SET id = '[23,24)' WHERE id = '[22,23)' AND valid_at @> '2019-01-01'::date;
+ERROR:  insert or update on table "tfkp2" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2018-01-01,2020-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,15)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [14,15) | [2018-01-01,2021-01-01) | [22,23)
+(1 row)
+
 --
 -- partitioned FK referenced deletes SET DEFAULT
 --
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[0,1)', daterange(null, null));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[14,15)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'), '[14,15)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,15)';
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2019-01-01,2020-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,10)';
+   id   |        valid_at         | parent_id 
+--------+-------------------------+-----------
+ [9,10) | [2018-01-01,2021-01-01) | [14,15)
+(1 row)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,15)';
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2018-01-01,2021-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,10)';
+   id   |        valid_at         | parent_id 
+--------+-------------------------+-----------
+ [9,10) | [2018-01-01,2021-01-01) | [14,15)
+(1 row)
+
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[24,25)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[24,25)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[15,16)', daterange('2018-01-01', '2021-01-01'), '[24,25)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,25)' AND valid_at @> '2019-01-01'::date;
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([-1,0), [2018-01-01,2020-01-01)) is not present in table "temporal_partitioned_rng".
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,16)';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [15,16) | [2018-01-01,2021-01-01) | [24,25)
+(1 row)
+
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
 --
@@ -2617,32 +4006,150 @@ 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
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[9,10)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[9,10)');
 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 SET NULL ON UPDATE SET NULL;
-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 = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[6,7)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [6,7) | {[2019-01-01,2020-01-01)}                         | 
+ [6,7) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [9,10)
+(2 rows)
+
+UPDATE temporal_partitioned_mltrng SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[6,7)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [6,7) | {[2019-01-01,2020-01-01)}                         | 
+ [6,7) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | 
+(2 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[18,19)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[18,19)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[12,13)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[18,19)');
+UPDATE temporal_partitioned_mltrng SET id = '[19,20)' WHERE id = '[18,19)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[12,13)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [12,13) | {[2018-01-01,2020-01-01)} | 
+ [12,13) | {[2020-01-01,2021-01-01)} | [18,19)
+(2 rows)
+
 --
 -- partitioned FK referenced deletes SET NULL
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[11,12)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[7,8)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[11,12)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[7,8)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [7,8) | {[2019-01-01,2020-01-01)}                         | 
+ [7,8) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [11,12)
+(2 rows)
+
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[7,8)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [7,8) | {[2019-01-01,2020-01-01)}                         | 
+ [7,8) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | 
+(2 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[20,21)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[20,21)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[13,14)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[20,21)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[20,21)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[13,14)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [13,14) | {[2018-01-01,2020-01-01)} | 
+ [13,14) | {[2020-01-01,2021-01-01)} | [20,21)
+(2 rows)
+
 --
 -- partitioned FK referenced updates SET DEFAULT
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[0,1)', datemultirange(daterange(null, null)));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[12,13)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[12,13)');
 ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
   ALTER COLUMN parent_id SET DEFAULT '[0,1)',
   DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
@@ -2650,10 +4157,67 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_mltrng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-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 = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[8,9)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [8,9) | {[2019-01-01,2020-01-01)}                         | [0,1)
+ [8,9) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [12,13)
+(2 rows)
+
+UPDATE temporal_partitioned_mltrng SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[8,9)';
+  id   |                     valid_at                      | parent_id 
+-------+---------------------------------------------------+-----------
+ [8,9) | {[2019-01-01,2020-01-01)}                         | [0,1)
+ [8,9) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [0,1)
+(2 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[22,23)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[22,23)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[14,15)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[22,23)');
+UPDATE temporal_partitioned_mltrng SET id = '[23,24)' WHERE id = '[22,23)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[14,15)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [14,15) | {[2018-01-01,2020-01-01)} | [0,1)
+ [14,15) | {[2020-01-01,2021-01-01)} | [22,23)
+(2 rows)
+
 --
 -- partitioned FK referenced deletes SET DEFAULT
 --
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[0,1)', datemultirange(daterange(null, null)));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[14,15)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[9,10)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[14,15)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[9,10)';
+   id   |                     valid_at                      | parent_id 
+--------+---------------------------------------------------+-----------
+ [9,10) | {[2019-01-01,2020-01-01)}                         | [0,1)
+ [9,10) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [14,15)
+(2 rows)
+
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[9,10)';
+   id   |                     valid_at                      | parent_id 
+--------+---------------------------------------------------+-----------
+ [9,10) | {[2019-01-01,2020-01-01)}                         | [0,1)
+ [9,10) | {[2018-01-01,2019-01-01),[2020-01-01,2021-01-01)} | [0,1)
+(2 rows)
+
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[24,25)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[24,25)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[15,16)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[24,25)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[24,25)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
+   id    |         valid_at          | parent_id 
+---------+---------------------------+-----------
+ [15,16) | {[2018-01-01,2020-01-01)} | [0,1)
+ [15,16) | {[2020-01-01,2021-01-01)} | [24,25)
+(2 rows)
+
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 RESET datestyle;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index b15679d675e..4bb6e27706d 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -1424,8 +1424,26 @@ ALTER TABLE temporal_fk_rng2rng
 --
 -- 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)
+-- referenced updates SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols, SET NULL subset)
+-- referenced updates SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
 
+--
 -- 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)');
@@ -1434,28 +1452,346 @@ 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:
+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;
+-- 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
     ON DELETE SET NULL ON UPDATE SET NULL;
+-- 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:
+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;
+-- 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 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)');
+-- 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 DEFAULT
+--
+
 TRUNCATE temporal_rng, temporal_fk_rng2rng;
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null));
 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
   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;
+-- 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:
+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;
+-- 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 SET DEFAULT
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null));
+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 update:
+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 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 FK referenced updates SET NULL (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 SET NULL ON UPDATE SET NULL;
+-- 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 SET NULL (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 FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+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)');
+-- fails because you can't set the PERIOD column:
+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 SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+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 SET NULL (parent_id1) ON UPDATE SET NULL;
+-- 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 FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', daterange(null, null));
+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
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)', id2 = '[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)', id2 = '[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)', id2 = '[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 SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', daterange(null, null));
+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 update:
+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 FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_rng2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', daterange(null, null));
+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)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_rng2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- 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 update:
+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_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', daterange(null, null));
+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
@@ -1855,6 +2191,408 @@ 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)
+-- referenced updates SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols)
+-- referenced deletes SET NULL (two scalar cols, SET NULL subset)
+-- referenced updates SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols)
+-- referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+
+--
+-- 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:
+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;
+-- 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 SET NULL
+--
+
+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 SET NULL ON UPDATE SET NULL;
+-- 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:
+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;
+-- 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 SET NULL
+--
+
+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 SET DEFAULT
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[-1,-1]', datemultirange(daterange(null, null)));
+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
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_mltrng
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- 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:
+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;
+-- 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 SET DEFAULT
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[-1,-1]', datemultirange(daterange(null, null)));
+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 update:
+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;
+
+--
+-- test FK referenced updates SET NULL (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 SET NULL ON UPDATE SET NULL;
+-- 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 SET NULL (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;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+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)');
+-- fails because you can't set the PERIOD column:
+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 SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+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 SET NULL (parent_id1) ON UPDATE SET NULL;
+-- 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;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', datemultirange(daterange(null, null)));
+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
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_mltrng2 FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id1 = '[7,8)', id2 = '[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)', id2 = '[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)', id2 = '[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 SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', datemultirange(daterange(null, null)));
+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 update:
+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;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_mltrng2, temporal_fk2_mltrng2mltrng;
+INSERT INTO temporal_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', datemultirange(daterange(null, null)));
+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)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_mltrng2mltrng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  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 SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- 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 update:
+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_mltrng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', datemultirange(daterange(null, null)));
+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 with a custom range type
+
+CREATE TYPE mydaterange AS range(subtype=date);
+
+CREATE TABLE temporal_rng3 (
+  id int4range,
+  valid_at mydaterange,
+  CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk3_rng2rng (
+  id int4range,
+  valid_at mydaterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk3_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk3_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng3 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[8,9)', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk3_rng2rng (id, valid_at, parent_id) VALUES ('[5,6)', mydaterange('2018-01-01', '2021-01-01'), '[8,9)');
+DELETE FROM temporal_rng3 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,9)';
+SELECT * FROM temporal_fk3_rng2rng WHERE id = '[5,6)';
+
+DROP TABLE temporal_fk3_rng2rng;
+DROP TABLE temporal_rng3;
+DROP TYPE mydaterange;
+
 --
 -- FK between partitioned tables: ranges
 --
@@ -1865,8 +2603,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'),
@@ -1880,8 +2618,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
@@ -1940,36 +2678,90 @@ 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
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)');
 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 SET NULL ON UPDATE SET NULL;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,7)';
+UPDATE temporal_partitioned_rng SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,7)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[18,19)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[18,19)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'), '[18,19)');
+UPDATE temporal_partitioned_rng SET id = '[19,20)' WHERE id = '[18,19)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,13)';
 
 --
 -- partitioned FK referenced deletes SET NULL
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[11,12)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[7,8)', daterange('2018-01-01', '2021-01-01'), '[11,12)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,8)';
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,8)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[20,21)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[20,21)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[13,14)', daterange('2018-01-01', '2021-01-01'), '[20,21)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,21)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,14)';
+
 --
 -- partitioned FK referenced updates SET DEFAULT
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[0,1)', daterange(null, null));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)');
 ALTER TABLE temporal_partitioned_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
   DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk,
@@ -1977,11 +2769,34 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_rng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,9)';
+UPDATE temporal_partitioned_rng SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,9)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[22,23)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[22,23)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[14,15)', daterange('2018-01-01', '2021-01-01'), '[22,23)');
+UPDATE temporal_partitioned_rng SET id = '[23,24)' WHERE id = '[22,23)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,15)';
 
 --
 -- partitioned FK referenced deletes SET DEFAULT
 --
 
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[0,1)', daterange(null, null));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[14,15)', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01'), '[14,15)');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,10)';
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,10)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[24,25)', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[24,25)', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[15,16)', daterange('2018-01-01', '2021-01-01'), '[24,25)');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,25)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,16)';
+
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
 
@@ -2070,36 +2885,90 @@ 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
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[9,10)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[6,7)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[9,10)');
 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 SET NULL ON UPDATE SET NULL;
+UPDATE temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[6,7)';
+UPDATE temporal_partitioned_mltrng SET id = '[10,11)' WHERE id = '[9,10)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[6,7)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[18,19)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[18,19)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[12,13)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[18,19)');
+UPDATE temporal_partitioned_mltrng SET id = '[19,20)' WHERE id = '[18,19)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[12,13)';
 
 --
 -- partitioned FK referenced deletes SET NULL
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[11,12)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[7,8)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[11,12)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[7,8)';
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[11,12)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[7,8)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[20,21)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[20,21)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[13,14)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[20,21)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[20,21)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[13,14)';
+
 --
 -- partitioned FK referenced updates SET DEFAULT
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[0,1)', datemultirange(daterange(null, null)));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[12,13)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[8,9)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[12,13)');
 ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
   ALTER COLUMN parent_id SET DEFAULT '[0,1)',
   DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
@@ -2107,11 +2976,34 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
     FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_partitioned_mltrng
     ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[8,9)';
+UPDATE temporal_partitioned_mltrng SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[8,9)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[22,23)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[22,23)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[14,15)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[22,23)');
+UPDATE temporal_partitioned_mltrng SET id = '[23,24)' WHERE id = '[22,23)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[14,15)';
 
 --
 -- partitioned FK referenced deletes SET DEFAULT
 --
 
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[0,1)', datemultirange(daterange(null, null)));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[14,15)', datemultirange(daterange('2018-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[9,10)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[14,15)');
+DELETE FROM temporal_partitioned_mltrng FOR PORTION OF valid_at (datemultirange(daterange('2019-01-01', '2020-01-01'))) WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[9,10)';
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[9,10)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[24,25)', datemultirange(daterange('2018-01-01', '2020-01-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[24,25)', datemultirange(daterange('2020-01-01', '2021-01-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[15,16)', datemultirange(daterange('2018-01-01', '2021-01-01')), '[24,25)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[24,25)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
+
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
-- 
2.47.3

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 20 Feb 2026 09:16:11 -0800

> Here is another round to fix a few rebase conflicts.

I realized we didn't have any tests for v18's new feature to say
`UPDATE ... RETURNING OLD.foo, NEW.foo`. These patches add a small
test for `RETURNING OLD.valid_at, NEW.valid_at` when you say `UPDATE
FOR PORTION OF valid_at`. This seems worth testing since that column
gets set in an automatic way, not via the normal SET syntax. No fixes
were needed.

I also corrected the commit message, which still referred to the
without_overlaps function that we renamed to
{range,multirange}_minus_multi.

As far as I know nothing else here is waiting on me, but please
correct me if I've overlooked something.

Rebased to 18bcdb75d1.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From f77d2c9c97aa4f0ba0812018459865b567f5df2c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 16:11:47 -0700
Subject: [PATCH v67 5/7] Look up additional temporal foreign key helper proc

 
 /*
- * FindFKPeriodOpers -
 
+	/*
 
 /*
index 2f5b7007ff9..ea92dc8129b 100644

index d22b8ef7f3c..c9017446f54 100644
 
@@ -2340,10 +2342,11 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	{
 		Oid			opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);

 	ReleaseSysCache(tup);

From 550767facbb9b470a5f22d5a08ab071221a6b1a0 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 31 Oct 2025 19:59:52 -0700
Subject: [PATCH v67 3/7] Add isolation tests for UPDATE/DELETE FOR PORTION OF

 
+  <para>
index 33ebd41fb99..a4f776be9c0 100644
@@ -1462,6 +1462,10 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,

From a3a528d65f4fe627248a8257dc3604bf9adcba17 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 2 Dec 2025 21:30:13 -0800
Subject: [PATCH v67 1/7] Add range_get_constructor2 to lsyscache

diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 1913b009d40..aa9d07e09c7 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3600,6 +3600,31 @@ get_range_collation(Oid rangeOid)
 		return InvalidOid;
 }
 
+/*

From e5a2fc81ede916d930dd1349d67ddd4eee0f62a2 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v67 4/7] Add tg_temporal to TriggerData

 
 
index 98d402c0a3b..c9229122118 100644

 

 

+/* ----------
+ * ForPortionOfState()
+ *

 /*
-- 
2.47.3

From f7b661bd9f4bb761645934285cdf3dd86d9a352e Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v67 2/7] Add UPDATE/DELETE FOR PORTION OF

To compute the temporal leftovers that are required, we use the *_minus_multi
set-returning functions defined in 5eed8ce50c.

 src/test/regress/expected/for_portion_of.out  | 2081 +++++++++++++++++
 src/test/regress/sql/for_portion_of.sql       | 1363 +++++++++++
 50 files changed, 5662 insertions(+), 90 deletions(-)

 

 INSERT INTO "S 1"."T 1"
 	SELECT id,
 
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";

 
 
+-- FOR PORTION OF setup
 
+-- FOR PORTION OF setup

 
 
 DITAA = ditaa
 DOT = dot
 
+  <para>

+  <para>
 
+   <varlistentry>
 
   <para>

 
+  <para>
 
+   <varlistentry>
 
   <para>

+   <para>
index 793c76d4f82..33ebd41fb99 100644

-

 /*
@@ -1382,6 +1389,235 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
@@ -1535,7 +1771,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1568,6 +1805,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1993,7 +2234,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2342,7 +2586,8 @@ lreplace:
@@ -2364,6 +2609,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL);
 	}
 
@@ -5291,6 +5540,101 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*

index 42604a0f75c..0ca79c46dd2 100644
 
index ef8ef6e89d3..151100f9840 100644
@@ -3651,7 +3651,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3717,6 +3717,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
 #include "postgres.h"

 

 
+/*
 
 /*

 
+		/*

 
 
 using_clause:
 
 set_clause_list:
 
+opt_alias:
+			AS ColId
+				{
 
+/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
index f16f1535785..9204a3e4f7c 100644

 

+/*
 
 /*
 
+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

 
+/*----------
index 00000000000..8fa96c3f246
@@ -0,0 +1,2081 @@
+-- UPDATE ... RETURNING supports NEW and OLD valid_at
+  FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20'
+  SET name = 'three^4'
+  RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at;
+  name   |  name   |        valid_at         |        valid_at         
+---------+---------+-------------------------+-------------------------
+ three^3 | three^4 | [2018-02-01,2018-02-15) | [2018-02-10,2018-02-15)
+ three   | three^4 | [2018-02-15,2018-06-01) | [2018-02-15,2018-02-20)
+ [3,4) | [2018-02-01,2018-02-10) | three^3
+ [3,4) | [2018-02-03,2018-02-10) | three^3
+ [3,4) | [2018-02-10,2018-02-15) | three^4
+ [3,4) | [2018-02-15,2018-02-20) | three^4
+ [3,4) | [2018-02-20,2018-06-01) | three
+(16 rows)
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 

 # ----------
index 00000000000..7230824ff5e
@@ -0,0 +1,1363 @@
+-- UPDATE ... RETURNING supports NEW and OLD valid_at
+  FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20'
+  SET name = 'three^4'
+  RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at;

 

 
 -- temporal UNIQUE:

 

 

 

 

 

 

 

 

 

 

 

 # cleanup

 

 

 

 # cleanup

 

 

 
index 241945734ec..512d714d6e4 100644

From 66461d86c4b7ac005c378143bee99168d67e5359 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v67 6/7] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
 keys

 
     <para>
 
          <para>
 
          <para>
 
          <para>
index ea92dc8129b..6eb8c982158 100644

index c9017446f54..ebe010d3d28 100644
 
@@ -233,6 +240,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -242,6 +250,11 @@ pg_noreturn static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,

 /*
@@ -455,6 +468,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					false,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
@@ -620,6 +634,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 	result = ri_PerformCheck(riinfo, &qkey, qplan,
 							 fk_rel, pk_rel,
 							 oldslot, NULL,
+							 -1, (Datum) 0,
 							 false,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
@@ -896,6 +911,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					!is_no_action,
 					true,		/* must detect new rows */
 					SPI_OK_SELECT);
@@ -998,6 +1014,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
@@ -1115,6 +1132,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
@@ -1343,6 +1361,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					false,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
@@ -1374,6 +1393,540 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
@@ -2490,6 +3043,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2502,8 +3056,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -2546,6 +3100,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -3226,6 +3786,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 		case F_RI_FKEY_CHECK_INS:
@@ -3235,3 +3801,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index dac40992cbc..9f825a2f178 100644
@@ -4130,6 +4130,28 @@

 
 explain (costs off)

 explain (costs off)
 
+--
 
+--
 
+--

+--
+
+--

 

 

 

 

 

 

 
-- 
2.47.3

From 62cc3f1859ef66e6c7cd5fc2f64918fb893fe47b Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v67 7/7] Expose FOR PORTION OF to plpgsql triggers

 

index 84552e32c87..6180161c970 100644

 
 /*
index 8fa96c3f246..53f7c744726 100644
@@ -1325,8 +1325,13 @@ CREATE FUNCTION dump_trigger()
 
   IF TG_ARGV[0] THEN
@@ -1376,10 +1381,10 @@ UPDATE for_portion_of_test
@@ -1406,19 +1411,19 @@ NOTICE:    new: [2022-01-01,2030-01-01)
@@ -1445,10 +1450,10 @@ NOTICE:    new: [2024-01-01,2030-01-01)
@@ -1516,10 +1521,10 @@ BEGIN;
@@ -1546,20 +1551,20 @@ NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
@@ -1574,10 +1579,10 @@ NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
@@ -1585,10 +1590,10 @@ BEGIN;
@@ -1603,10 +1608,10 @@ NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
@@ -1643,7 +1648,7 @@ NOTICE:    new: [2018-01-01,2018-01-15)
@@ -1653,10 +1658,10 @@ COMMIT;
index 7230824ff5e..e166c29f28b 100644
@@ -880,8 +880,13 @@ CREATE FUNCTION dump_trigger()
 
   IF TG_ARGV[0] THEN

---

From: Kirill Reshke <[email-masked]>
Date: Tue, 10 Mar 2026 17:26:30 +0500

> correct me if I've overlooked something.

Hi!
v67-0001 looks good to me.

When applying first two of patches from v67 series, my initdb fails:

```
reshke@yezzey-cbdb-bench:~/cpg$ ./bin/bin/initdb -D ./db
The files belonging to this database system will be owned by user "reshke".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are enabled.

creating directory db ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... 2026-03-10 12:21:05.842
UTC [2995664] WARNING:  unrecognized node type: 155
2026-03-10 12:21:05.842 UTC [2995664] FATAL:  unrecognized node type: 155
2026-03-10 12:21:05.842 UTC [2995664] STATEMENT:  REVOKE ALL ON
pg_authid FROM public;

child process exited with exit code 1
initdb: removing data directory "db"
```

without v67-0002 initdb runs ok.

Also, after v67-0002 my createdb fails:

```
reshke@yezzey-cbdb-bench:~/cpg$ ./bin/bin/createdb
createdb: error: query failed: ERROR:  syntax error at or near "("
LINE 1: SELECT pg_catalog.set_config('search_path', '', false);
                                    ^
createdb: detail: Query was: SELECT
pg_catalog.set_config('search_path', '', false);
```

Simple queries also fails:
```
postgres=# select now();
WARNING:  unrecognized node type: 144
ERROR:  unrecognized node type: 76
```

--
Best regards,
Kirill Reshke

---

From: Paul A Jungwirth <[email-masked]>
Date: Tue, 10 Mar 2026 09:13:00 -0700

> ERROR:  unrecognized node type: 76

I don't see any of these problems here (after an error-free rebase
onto a198c26ded), and CI passes. Are you sure that was from a clean
build? If so, could you share your configure line?

Thanks,

---

From: Kirill Reshke <[email-masked]>
Date: Tue, 10 Mar 2026 22:33:30 +0500

> build? If so, could you share your configure line?

Sorry, It was indeed an issue on my side. It all gone after make
maintainer-clean.
Anyway, I was interested in by-hand testing of 0001 & 0002, which I
did. I tested various partitioned table use-cases, including the new
MERGE PARTITIONS feature, updating the partition column, etc. All
seems to work just fine.

The only review comment I have is that we may need tab-completion
support for UPDATE ... [FOR PARTITION OF] pattern.

-- 
Best regards,
Kirill Reshke

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 13 Mar 2026 10:06:50 -0700

On Thu, Mar 12, 2026 at 1:39 AM Peter Eisentraut <[email-masked]> wrote:
> Hi Paul,
>
> Review of v67-0001-Add-range_get_constructor2-to-lsyscache.patch and
> v67-0002-Add-UPDATE-DELETE-FOR-PORTION-OF.patch:

Thanks for taking another look! v68 patches attached, details below:

> 1) In src/backend/parser/analyze.c, transformForPortionOfClause():
>
> The variable range_name is not very useful; using
> forPortionOf->range_name is clearer.

Okay, done.

> Try making the forPortionOf argument const.

Done.

> The initialization strat = RTOverlapStrategyNumber; is confusing.

You're right. That was left over from when GetOperatorFromCompareType
took an in/out param. Now it's purely out.

> 2) src/backend/parser/gram.y
>
> Explain the %prec in opt_alias with a comment.  Maybe the production
> name should be more specific if it's only applicable to some specific
> statement.

Added an explanation and renamed the production.

> 3) In the test src/test/regress/expected/for_portion_of.out:
>
> 3.1)
>
> Fix the wording of the error message here (remove "period", add back in
> later patch):
>

Done. I've added a commit to restore "or period" to my private PERIOD
branch, but it's not part of this series anymore. I'll introduce it
later.

> 3.2)
>
> Could this error message have a location pointer?
>

This is harder than I thought. It happens inside
contain_volatile_functions_after_planning. Even if I omit that call
entirely, it still happens during planning from
eval_const_expressions. Is there some way to pass a location down that
far, e.g. with soft error context? I don't see how to do it.

> 3.3)
>
> Improve wording of this error message:
>
> ("FOR PORTION OF target was null"?)

Changed to your suggestion. Also this should be an ereport, not elog,
since the (...) syntax would let a user give a NULL directly. Also I
added a location pointer.

> 3.4)
> This test updates the id column but the SELECT afterwards only shows
> rows for the old id value.  The SELECT ought to use something like
>
> WHERE id = '[1,2)' OR id = '[6,7)'

Okay, done.

> 3.5)
>
> -- UPDATE FOR PORTION OF in a CTE:
>
> I think this test is meant to show that the UPDATE FOR PORTION OF
> ... RETURNING returns only the updated rows, not the inserted
> "leftovers".
>
> First, it would be good if the comment was more clear about what it's
> trying to demonstrate.
>
> And then, is there a reason for this behavior versus the alternatives?
> SQL standard, other implementations?

My goal here wasn't to test RETURNING per se, but just to see if FOR
PORTION OF composed with CTEs properly. (There are a bunch of small
tests in this area of the file testing composition with other
features.) But a CTE has to return *something*, so I had to put
RETURNING there. I was especially interested in MVCC visibility, both
of the update changes (including automatically updating valid_at) and
the leftover inserts. I added a comment to the test file with all
that.

As you say, there is another test below that focuses more on RETURNING
(as a top-level statement, not inside a CTE). I'll address your
questions about RETURNING behavior down there.

> 3.6)
> Is this behavior the same or different from the way normal queries
> work?  Could be clarified in the comment either way.

I expanded the comment. It's the same as how other queries work.

> 3.7)
> This test looks redundant with earlier tests.  Otherwise, maybe add a
> comment about how it's different.

I don't think a top-level RETURNING test is redundant with the CTE
test. I expanded the comment here a bit to clarify the goal. It
addresses your question above: Should RETURNING include the inserted
leftovers? I don't think that makes sense:

1. Our docs say, "The optional RETURNING clause causes UPDATE to
compute and return value(s) based on each row actually updated." The
leftovers were not updated.

2. Conceptually, the leftovers represent what *didn't* change.

3. If you implemented this with a trigger, you also wouldn't get the
inserted leftovers.

4. The SQL standard doesn't have RETURNING. But it does say that to
insert the leftovers the system should execute a separate insert
"statement". So we should do something very similar to the trigger
case.

5. I tried comparing our behavior to MariaDB and IBM DB2. MariaDB
doesn't have RETURNING, so it's no help. DB2 has RETURNING INTO inside
PL/SQL, but I couldn't get it to work. If I do, I'll update this
thread.

6. Omitting the leftovers is consistent with what we're doing for
firing update/insert row/statement triggers and what we're putting in
the transition trigger tables.

7. If we included leftovers in UPDATE RETURNING, we should include
them in DELETE RETURNING, and that makes even less sense.

Personally, as a user of this feature, getting the leftovers back from
RETURNING would be very unexpected and annoying. So I think we are
doing the right thing here.

> 3.8)
> Please indent the non-first rows of the CREATE TRIGGER statements.

Done.

> 4) In src/test/subscription/t/034_temporal.pl
>
> The change from FULL to DEFAULT seems wrong.

You're right; fixed.

> 5) NULL bounds
>
> A general comment: In particular after studying these tests in detail,
> I'm suspicious that it's a good idea to interpret null bounds as
> unbounded.  Expressions could return null for nested reasons, it would
> be very hard to follow that.  Null values should mean "unknown",
> unbounded should be explicit.  We have the keyword UNBOUNDED already,
> maybe you could use that?  Or do you want to be able to return
> unboundedness from an expression?

I like the idea of a keyword. I tried adding UNBOUNDED but it caused a
few hundred S/R and R/R conflicts that I couldn't easily resolve. A
year or two ago I had keywords here (MINVALUE/MAXVALUE IIRC), but it
required some nasty parser hacks. This is a pretty delicate area of
the grammar, because we have a_expr with FROM and TO and no
punctuation. I'm already doing some contortions to handle `FOR PORTION
OF valid_at FROM t1 + INTERVAL '1' YEAR TO MONTH TO t2`.

A keyword is not offered by the standard here, so it would just be
custom syntactic sugar. No other RDBMS has one (I think).

I think NULL is the right choice for unbounded. It is what range types
use, and we want this to mesh well with them. More important it works
for *any type*. We don't always have +/-Infinity.

Also I think we should expand user choice rather than restrict it. If
users want to forbid nulls, they can (e.g. by using a domain type).
But if we forbid it, there is no way to override that decision.

Going back to the UNBOUNDED keyword: if we forbid nulls, then a
keyword doesn't really add clarity, since users would already say
`-Infinity` or `Infinity`. It's really just a way to express what null
means in this context. Assuming we keep nulls, I'd like to keep
working on a keyword. But I think we could add it later.

Btw what do you think of the READ COMMITTED issues I brought up in my
third patch? We follow MariaDB here, but not DB2. DB2's behavior is
less problematic for users, although their isolation levels don't
quite match ours. If we're not okay with those results, we should
address them before merging the main patch.

Rebased to 1c33a2d81d.

Yours,

--
Paul              ~{:-)
[email-masked]
From bc1abfb36c01a28f1f8114884afca77dc918a7e1 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v68 2/7] Add UPDATE/DELETE FOR PORTION OF

 src/backend/executor/nodeModifyTable.c        |  358 ++-
 src/backend/parser/gram.y                     |  111 +-
 src/include/nodes/parsenodes.h                |   21 +
 src/include/nodes/primnodes.h                 |   35 +
 src/test/regress/expected/for_portion_of.out  | 2100 +++++++++++++++++
 src/test/regress/sql/for_portion_of.sql       | 1368 +++++++++++
 src/test/subscription/t/034_temporal.pl       |   83 +-
 50 files changed, 5706 insertions(+), 89 deletions(-)

index 0f5271d476e..ac34a1acacb 100644
 
 -- Test that alteration of server options causes reconnection
 -- Remote's errors might be non-English, so hide them to ensure stable results
@@ -6311,6 +6333,27 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass;
  ft2
 (1 row)
 
index 49ed797e8ef..0e218b29a29 100644

 INSERT INTO "S 1"."T 1"
 	SELECT id,
 
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";

@@ -1553,6 +1576,17 @@ EXPLAIN (verbose, costs off)

+-- FOR PORTION OF setup
 
+-- FOR PORTION OF setup

 
 
 DITAA = ditaa
 DOT = dot
index 77066ef680b..98f72730e11 100644
@@ -432,6 +432,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
+  <para>

+  <para>
 
+   <varlistentry>
 
   <para>

 
+  <para>
 
+   <varlistentry>
 
   <para>

+   <para>
index 327c27abff9..ce961ce3ae9 100644

-

 /*

 
 
@@ -5296,6 +5545,107 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+		 * FOR PORTION OF ... TO ... FROM should never give us a NULL target,
+		 * but FOR PORTION OF (...) could.
+					(errmsg("FOR PORTION OF target was null")),
+					executor_errposition(estate, forPortionOf->targetLocation));
index 50b0e10308b..c7bc41c30d7 100644
@@ -315,7 +315,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 
@@ -2676,6 +2676,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->onconflict,
 							best_path->mergeActionLists,
 							best_path->mergeJoinConditions,
+							best_path->forPortionOf,
 							best_path->epqParam);
 
@@ -7009,7 +7010,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
@@ -7082,6 +7083,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 
index 96cc72a776b..73518c8f870 100644
@@ -3698,7 +3698,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3764,6 +3764,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
index 15cc6240203..1af0d932293 100644
 
 #include "postgres.h"

+													 const ForPortionOfClause *forPortionOfClause,
@@ -607,6 +617,12 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)

@@ -1250,7 +1266,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		/* Process the SELECT/UPDATE WHERE clause */
 		onConflictWhere = transformWhereClause(pstate,
@@ -1282,6 +1298,321 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+							const ForPortionOfClause *forPortionOf,
+	range_attno = attnameAttNum(targetrel, forPortionOf->range_name, false);
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						forPortionOf->range_name,
+							forPortionOf->range_name,
+							forPortionOf->range_name,
+		tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno,
+							  forPortionOf->range_name, false);
+	result->range_name = forPortionOf->range_name;
+	result->location = forPortionOf->location;
+	result->targetLocation = forPortionOf->target_location;
+
+	return result;
+}
 
 /*
@@ -2541,6 +2872,13 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2567,7 +2905,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2586,7 +2925,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2639,6 +2978,20 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index f01f5734fe9..91913f53d71 100644
@@ -558,6 +558,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+%type <alias>	for_portion_of_opt_alias
 
@@ -769,7 +771,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER

@@ -888,12 +890,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -12723,6 +12728,21 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+			| opt_with_clause DELETE_P FROM relation_expr
+			for_portion_of_clause for_portion_of_opt_alias
 
 using_clause:
@@ -12797,6 +12817,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
+			| opt_with_clause UPDATE relation_expr
+			for_portion_of_clause for_portion_of_opt_alias
+			SET set_clause_list
+			from_clause
+			where_or_current_clause
+			returning_clause
+				{
+					UpdateStmt *n = makeNode(UpdateStmt);
+
+					n->relation = $3;
+					n->forPortionOf = (ForPortionOfClause *) $4;
+					n->relation->alias = $5;
+					n->targetList = $7;
+					n->fromClause = $8;
+					n->whereClause = $9;
+					n->returningClause = $10;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 set_clause_list:
@@ -14299,6 +14338,55 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+/*
+ * If an UPDATE/DELETE has FOR PORTION OF, then the relation_expr is separated
+ * from its potential alias by the for_portion_of_clause. So this production
+ * handles the potential alias in those cases. We need to solve the same
+ * problems as relation_expr_opt_alias, in particular resolving a shift/reduce
+ * conflict where "set set" could be an alias plus the SET keyword, or the SET
+ * keyword then a column name. As above, we force the latter interpretation by
+ * giving the non-alias choice a higher precedence.
+ */
+for_portion_of_opt_alias:
+			AS ColId
+				{
+					Alias	   *alias = makeNode(Alias);
+
+					alias->aliasname = $2;
+					$$ = alias;
+				}
+			| BareColLabel
+				{
+					Alias	   *alias = makeNode(Alias);
+
+					alias->aliasname = $1;
+					$$ = alias;
+				}
+			| /* empty */ %prec UMINUS { $$ = NULL; }
+		;
+
+for_portion_of_clause:
+			FOR PORTION OF ColId '(' a_expr ')'
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->location = @4;
+					n->target = $6;
+					n->target_location = @6;
+					$$ = (Node *) n;
+				}
+			| FOR PORTION OF ColId FROM a_expr TO a_expr
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->location = @4;
+					n->target_start = $6;
+					n->target_end = $8;
+					n->target_location = @5;
+					$$ = (Node *) n;
+				}
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -15139,16 +15227,25 @@ opt_timezone:
 			| /*EMPTY*/								{ $$ = false; }
 		;
 
+/*
@@ -18224,6 +18321,7 @@ unreserved_keyword:
@@ -18858,6 +18956,7 @@ bare_label_keyword:
index 33fd2cccae5..82816666421 100644
@@ -584,6 +584,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 
 			/*
@@ -1024,6 +1031,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
index 96991cae764..d22349a6782 100644
 
 			/*
 
 			/*
 
 			/*
 
 			/*
index f98062668d6..104e9f6c48f 100644
 
index 6298a37f88e..00b3d69a862 100644
@@ -7197,6 +7199,9 @@ get_update_query_def(Query *query, deparse_context *context)

@@ -7401,6 +7406,9 @@ get_delete_query_def(Query *query, deparse_context *context)
 					 only_marker(rte),
 					 generate_relation_name(rte->relid, NIL));

@@ -12770,6 +12778,39 @@ get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
 						 quote_identifier(refname));
 }
 
+/*
 
 /*
 
+/*
+ * ForPortionOfState
+ *

index f3d32ef0188..5adb7ea113e 100644
 
@@ -1641,6 +1644,22 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <range-name> FROM <target-start> TO
+ *		<target-end> or FOR PORTION OF <range-name> (<target>)
+ */
+typedef struct ForPortionOfClause
+{
+	NodeTag		type;
+	char	   *range_name;		/* column name of the range/multirange */
+	ParseLoc	location;		/* token location, or -1 if unknown */
+	ParseLoc	target_location;	/* token location, or -1 if unknown */
+	Node	   *target;			/* Expr from FOR PORTION OF col (...) syntax */
+	Node	   *target_start;	/* Expr from FROM ... TO ... syntax */
+	Node	   *target_end;		/* Expr from FROM ... TO ... syntax */
+} ForPortionOfClause;
+
 /*
  * WithClause -
  *	   representation of WITH clause
@@ -2155,6 +2174,7 @@ typedef struct DeleteStmt
 	Node	   *whereClause;	/* qualifications */
 	ReturningClause *returningClause;	/* RETURNING clause */
 	WithClause *withClause;		/* WITH clause */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
 } DeleteStmt;
 
@@ -2170,6 +2190,7 @@ typedef struct UpdateStmt
 
index 27758ec16fe..3be25e0b142 100644
@@ -2709,6 +2709,7 @@ typedef struct ModifyTablePath
index 384df50c80a..99a52856a46 100644
@@ -2391,4 +2391,39 @@ typedef struct OnConflictExpr
 
+/*----------
+	ParseLoc	location;		/* token location, or -1 if unknown */
+	ParseLoc	targetLocation;	/* token location, or -1 if unknown */
index da2d9b384b5..e8db321f92b 100644
@@ -319,7 +319,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
index 6f74a8c05c7..490f4848af1 100644
index 00000000000..8a29a19f501
@@ -0,0 +1,2100 @@
+ERROR:  column "invalid_at" of relation "for_portion_of_test" does not exist
+ERROR:  FOR PORTION OF target was null
+LINE 2:   FOR PORTION OF valid_at (NULL)
+SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at;
+-- The outer query sees the table how it was before the updates,
+-- and with no leftovers yet,
+-- but it also sees the new values via the RETURNING clause.
+-- (We test RETURNING more directly, without a CTE, below.)
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at;
+-- (This is the same behavior as without FOR PORTION OF.)
+SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at;
+ERROR:  column "invalid_at" of relation "for_portion_of_test" does not exist
+ERROR:  FOR PORTION OF target was null
+LINE 2:   FOR PORTION OF valid_at (NULL)
+-- UPDATE ... RETURNING returns only the updated values
+-- (not the inserted side values, which are added by a separate "statement"):
+-- DELETE ... RETURNING returns the deleted values, regardless of bounds
+-- (not the inserted side values, which are added by a separate "statement"):
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true);
+  AFTER UPDATE ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true);
+  AFTER DELETE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true);
+  AFTER UPDATE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true);
+  AFTER DELETE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+ERROR:  FOR PORTION OF target was null
+LINE 2:   FOR PORTION OF valid_at (NULL)
+ERROR:  FOR PORTION OF target was null
+LINE 2:   FOR PORTION OF valid_at (NULL)
index 7bc274566c3..e5350b1df65 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 

 # ----------
index 00000000000..20d7e879c14
@@ -0,0 +1,1368 @@
+SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at;
+-- The outer query sees the table how it was before the updates,
+-- and with no leftovers yet,
+-- but it also sees the new values via the RETURNING clause.
+-- (We test RETURNING more directly, without a CTE, below.)
+SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at;
+-- (This is the same behavior as without FOR PORTION OF.)
+SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at;
+-- UPDATE ... RETURNING returns only the updated values
+-- (not the inserted side values, which are added by a separate "statement"):
+-- DELETE ... RETURNING returns the deleted values, regardless of bounds
+-- (not the inserted side values, which are added by a separate "statement"):
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true);
+  AFTER UPDATE ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true);
+  AFTER DELETE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table
+  FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false);
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  REFERENCING NEW TABLE AS new_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true);
+  AFTER UPDATE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true);
+  AFTER DELETE ON for_portion_of_test
+  REFERENCING OLD TABLE AS old_table
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false);
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update();
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
+  AFTER DELETE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete();
index 540f73ea9b1..507e68f29d8 100644

 

 
 -- temporal UNIQUE:

 

 

 
index 66955e1b799..841613da936 100644

 

 

 

 

 

 

 
 # replicate with a unique key:
@@ -333,16 +365,22 @@ $node_publisher->safe_psql(

 
 
 [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique FULL');
 
 # cleanup

 

 

 
 
 # cleanup

 

 

 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 49ad84a62d4..5fa39d0ee21 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -855,6 +855,9 @@ ForBothState
 ForEachState
 ForFiveState
 ForFourState
+ForPortionOfClause
+ForPortionOfExpr
+ForPortionOfState
 ForThreeState
 ForeignAsyncConfigureWait_function
 ForeignAsyncNotify_function
-- 
2.47.3

From c1e25a17d357f96b7f035de26a3357dece684902 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 2 Dec 2025 21:30:13 -0800
Subject: [PATCH v68 1/7] Add range_get_constructor2 to lsyscache

diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index f10948483b9..a1088e7225a 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3668,6 +3668,31 @@ get_range_collation(Oid rangeOid)
 		return InvalidOid;
 }
 
+/*
index b9ad84ecd41..5a1f38dba7e 100644
@@ -202,6 +202,7 @@ extern char *get_namespace_name(Oid nspid);

From f75a953a439e0820517aa8be2ebf4eaa8afe1e10 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v68 4/7] Add tg_temporal to TriggerData

 

 

 

+/* ----------
+ * ForPortionOfState()
+ *

 /*
-- 
2.47.3

From f9bec8c25e5313524e203bfbf65153ebada400dd Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 16:11:47 -0700
Subject: [PATCH v68 5/7] Look up additional temporal foreign key helper proc

 
 /*
- * FindFKPeriodOpers -
 
+	/*
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cd6d720386f..aeb1d64f3f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10632,9 +10632,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			intersectprocoid;

index 1af0d932293..ad9e2c9692e 100644
@@ -1549,7 +1549,7 @@ transformForPortionOfClause(ParseState *pstate,

 
 	ReleaseSysCache(tup);
index 1b7fedf1750..479a9a653db 100644
@@ -292,10 +292,11 @@ extern void DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,

From a0f0f7c73184d80a8ccabb16c0b3da1af889d49d Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 31 Oct 2025 19:59:52 -0700
Subject: [PATCH v68 3/7] Add isolation tests for UPDATE/DELETE FOR PORTION OF

 
+  <para>
index ce961ce3ae9..ecb4e8edc37 100644

From 0d35fd41f8408d28aed4c973cafd81d0b8b52ce1 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v68 7/7] Expose FOR PORTION OF to plpgsql triggers

 
index 5ecc7766757..12eebfa3617 100644

 

 /*
index 8a29a19f501..e99340293e5 100644
@@ -1340,8 +1340,13 @@ CREATE FUNCTION dump_trigger()
 
   IF TG_ARGV[0] THEN
@@ -1391,10 +1396,10 @@ UPDATE for_portion_of_test
@@ -1421,19 +1426,19 @@ NOTICE:    new: [2022-01-01,2030-01-01)
@@ -1460,10 +1465,10 @@ NOTICE:    new: [2024-01-01,2030-01-01)
@@ -1531,10 +1536,10 @@ BEGIN;
@@ -1561,20 +1566,20 @@ NOTICE:    new: ("[1,2)","[2019-01-01,2020-01-01)",one)
@@ -1589,10 +1594,10 @@ NOTICE:    new: ("[1,2)","[2018-01-21,2020-01-01)",one)
@@ -1600,10 +1605,10 @@ BEGIN;
@@ -1618,10 +1623,10 @@ NOTICE:    new: ("[1,2)","[2018-01-02,2020-01-01)",one)
@@ -1658,7 +1663,7 @@ NOTICE:    new: [2018-01-01,2018-01-15)
@@ -1668,10 +1673,10 @@ COMMIT;
index 20d7e879c14..a2808b15ba8 100644
@@ -885,8 +885,13 @@ CREATE FUNCTION dump_trigger()
 
   IF TG_ARGV[0] THEN

From 8d571317c930b7afa7e326076d8a973f62cca790 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v68 6/7] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
 keys

 
     <para>
index 982532fe725..f03e5a32d73 100644
@@ -1316,7 +1316,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1331,7 +1333,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1348,7 +1353,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
index aeb1d64f3f4..5203e069da4 100644
@@ -570,7 +570,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
@@ -10142,6 +10142,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -10227,15 +10228,20 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10337,19 +10343,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10705,6 +10705,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -10718,6 +10719,14 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
@@ -14123,17 +14132,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
@@ -14183,17 +14201,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr

 /*

+/*
 
 	/*
 
 	/*

 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index 361e2cfffeb..dc62c6c9b0a 100644
@@ -4133,6 +4133,28 @@

 
 explain (costs off)

 explain (costs off)
 
+--
 
+--
 
+--

+--
+
+--

 

 

 

 

 

 

 
-- 
2.47.3

---

From: Paul A Jungwirth <[email-masked]>
Date: Tue, 17 Mar 2026 07:29:07 -0700

> Thanks for taking another look! v68 patches attached, details below:

This needed a rebase, so here it is. Now rebased up to c9babbc881.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From 0e7b4d688b90063c2219eaebf9c80da9384beb72 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 2 Dec 2025 21:30:13 -0800
Subject: [PATCH v69 1/7] Add range_get_constructor2 to lsyscache

diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 768b11e3b82..160065fd9eb 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -3670,6 +3670,31 @@ get_range_collation(Oid rangeOid)
 		return InvalidOid;
 }
 
+/*
index 71b1a8f277d..e57795fa01f 100644

From ba49339364a0c27410a34a2149180b772b34d003 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v69 4/7] Add tg_temporal to TriggerData

 
 
index 9c0438a125a..5bce48a0a9d 100644
@@ -49,12 +49,14 @@
@@ -2651,6 +2653,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
@@ -2759,6 +2762,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
@@ -2860,6 +2864,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,

@@ -2923,6 +2928,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];
@@ -3066,6 +3072,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++)
@@ -3228,6 +3235,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -3699,6 +3707,7 @@ typedef struct AfterTriggerSharedData
@@ -3962,6 +3971,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);

@@ -4169,6 +4179,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
@@ -4539,6 +4550,9 @@ AfterTriggerExecute(EState *estate,

@@ -6125,6 +6139,42 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6558,6 +6608,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,

 /*
-- 
2.47.3

From 5eef2d0511ee5d014dbf642e51750ecba5449eaa Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v69 2/7] Add UPDATE/DELETE FOR PORTION OF

 src/include/nodes/execnodes.h                 |   23 +-
 50 files changed, 5706 insertions(+), 90 deletions(-)

 

 INSERT INTO "S 1"."T 1"
 	SELECT id,
 
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";

 
 
+-- FOR PORTION OF setup
 
+-- FOR PORTION OF setup

 
 
 DITAA = ditaa
 DOT = dot
 
+  <para>

+  <para>
 
+   <varlistentry>
 
   <para>

 
+  <para>
 
+   <varlistentry>
 
   <para>

+   <para>
index 58b84955c2b..45e00c6af85 100644
@@ -1314,6 +1314,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index 4cd5e262e0f..aae753b6ce7 100644
@@ -71,6 +71,7 @@
 
@@ -134,7 +135,6 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
-
@@ -167,6 +167,10 @@ static bool ExecOnConflictSelect(ModifyTableContext *context,
@@ -189,6 +193,9 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,

 /*

 
@@ -2366,6 +2611,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL);
 	}
 
@@ -5298,6 +5547,107 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index 6a850349cf7..c0b880ec233 100644
@@ -2579,6 +2579,20 @@ expression_tree_walker_impl(Node *node,
@@ -2747,6 +2761,8 @@ query_tree_walker_impl(Query *query,
@@ -3647,6 +3663,22 @@ expression_tree_mutator_impl(Node *node,
@@ -3828,6 +3860,7 @@ query_tree_mutator_impl(Query *query,

 
 
index ad31dee2686..f0e9ebaddd5 100644
 
 #include "postgres.h"

 

 
+/*
 
 /*
@@ -2538,6 +2869,13 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2564,7 +2902,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2583,7 +2922,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2636,6 +2975,20 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index c2584249603..4807217faa4 100644
@@ -559,6 +559,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -800,7 +802,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PROPERTIES PROPERTY PUBLICATION
 
@@ -919,12 +921,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %left		Op OPERATOR RIGHT_ARROW '|'	/* multi-character ops and user-defined operators */
@@ -13169,6 +13174,21 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 
 using_clause:
@@ -13243,6 +13263,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
 
 set_clause_list:
@@ -14756,6 +14795,55 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+/*
@@ -15596,16 +15684,25 @@ opt_timezone:
 
+/*
@@ -18903,6 +19000,7 @@ unreserved_keyword:
@@ -19547,6 +19645,7 @@ bare_label_keyword:
index 6076e9373c1..acb933392de 100644
 
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			if (isAgg)
@@ -1035,6 +1042,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("window functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("window functions are not allowed in FOR PORTION OF expressions");
+			break;
 
 			/*
index de4b20cd6af..022b8cac122 100644
index 474caffad48..a3b985065ed 100644
@@ -588,6 +588,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1880,6 +1883,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("cannot use subquery in property definition expression");
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("cannot use subquery in FOR PORTION OF expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3241,6 +3247,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "CYCLE";
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			return "property definition expression";
+		case EXPR_KIND_FOR_PORTION:
+			return "FOR PORTION OF";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 8dbd41a3548..35ff6427147 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2786,6 +2786,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
 			err = _("set-returning functions are not allowed in property definition expressions");
 			break;
+		case EXPR_KIND_FOR_PORTION:
+			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
+			break;
 
 			/*
index e33fd81d735..021c73f1b67 100644
@@ -3757,6 +3757,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
@@ -4113,6 +4137,37 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
@@ -4158,7 +4213,25 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
index 7bc12589e40..bed7c198b1d 100644
@@ -524,6 +524,8 @@ static void get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
@@ -7553,6 +7555,9 @@ get_update_query_def(Query *query, deparse_context *context)

@@ -7757,6 +7762,9 @@ get_delete_query_def(Query *query, deparse_context *context)
 					 only_marker(rte),
 					 generate_relation_name(rte->relid, NIL));

@@ -13330,6 +13338,39 @@ get_rte_alias(RangeTblEntry *rte, int varno, bool use_as,
 						 quote_identifier(refname));
 }
 
+/*
index 0716c5a9aed..7777fb5d72c 100644
@@ -39,7 +39,7 @@
 #include "partitioning/partdefs.h"
 #include "storage/buf.h"
 #include "utils/reltrigger.h"
 
 /*
  * forward references in this file
@@ -464,6 +464,24 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -600,6 +618,9 @@ typedef struct ResultRelInfo

index ffadd667167..c6a0796bd42 100644
 
@@ -1697,6 +1700,22 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -2211,6 +2230,7 @@ typedef struct DeleteStmt
 
@@ -2226,6 +2246,7 @@ typedef struct UpdateStmt
 
index b6185825fcb..0e599595162 100644
@@ -374,6 +374,8 @@ typedef struct ModifyTable
index 6fdf8807533..28f8cd8dd3e 100644
@@ -2415,4 +2415,39 @@ typedef struct OnConflictExpr
 
+/*----------
index e10270ff0ff..92c1c502945 100644
index b7ded6e6088..51ead54f015 100644
@@ -353,6 +353,7 @@ PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
index fc2cbeb2083..bf5ccf4c885 100644
index 9c9cdd12af5..d6c364b213d 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 
index e779ada70cb..b1426abe493 100644
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of
 
 # ----------
index e34c65fc1b2..88520efbcaf 100644

 

 
 -- temporal UNIQUE:

 

 

 

 

 

 

 

 

 

 

 

 # cleanup

 

 

 
 
 # cleanup

 

 

 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 52f8603a7be..8ebcdfe41e4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -858,6 +858,9 @@ ForBothState
 ForEachState
 ForFiveState
 ForFourState
+ForPortionOfClause
+ForPortionOfExpr
+ForPortionOfState
 ForThreeState
 ForeignAsyncConfigureWait_function
 ForeignAsyncNotify_function
-- 
2.47.3

From c0d8fbf1c23cd9ece86b6bf7b3d4eb6c29a350db Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 31 Oct 2025 19:59:52 -0700
Subject: [PATCH v69 3/7] Add isolation tests for UPDATE/DELETE FOR PORTION OF

 
+  <para>
index aae753b6ce7..e16299cc2ed 100644

From 9f27a5f0f9ed3d1fd5fc6ffc6bfdd4224936a135 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 16:11:47 -0700
Subject: [PATCH v69 5/7] Look up additional temporal foreign key helper proc

 
 /*
- * FindFKPeriodOpers -
 
+	/*
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 67e42e5df29..e6e8462d3c2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10647,9 +10647,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			intersectprocoid;

index f0e9ebaddd5..7023605e119 100644
@@ -1546,7 +1546,7 @@ transformForPortionOfClause(ParseState *pstate,

 
 	ReleaseSysCache(tup);

From ac1de710e0708e92374850fd1b68e8ab1be39fe6 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v69 7/7] Expose FOR PORTION OF to plpgsql triggers

 
index b72c963b3be..0b470bae724 100644

 

 /*
 
   IF TG_ARGV[0] THEN
 
   IF TG_ARGV[0] THEN

From 63c1d550a35cfb7cb91aa8564671caa3633125bb Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v69 6/7] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
 keys

index 8421ecace1b..ac8fdb183fe 100644
 
     <para>
 
          <para>
 
          <para>
 
          <para>
index e6e8462d3c2..49e7e5f639d 100644
@@ -577,7 +577,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
@@ -10157,6 +10157,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -10242,15 +10243,20 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10352,19 +10358,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10720,6 +10720,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -10733,6 +10734,14 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
@@ -14138,17 +14147,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
@@ -14198,17 +14216,26 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr

 /*

+/*
 
 	/*
 
 	/*

 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index fc8d82665b8..25689f9b3c7 100644
@@ -4136,6 +4136,28 @@

 
 explain (costs off)

 explain (costs off)
 
+--
 
+--
 
+--

+--
+
+--

 

 

 

 

 

 

 
-- 
2.47.3

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 25 Mar 2026 17:05:35 +0100

> "statement". So we should do something very similar to the trigger

UPDATE ... RETURNING is in my mind equivalent to the SQL standard SELECT 
... FROM NEW TABLE (UPDATE ...) (see <data change delta table>).  So I 
was hoping for an answer there, but it just says:

"If TP simply contains a <data change delta table> DCDT, then let S be 
the <data change statement> simply contained in TP. S shall not contain 
FOR PORTION OF."

So we can pick our own behavior.  Your explanation makes sense.  (I 
suppose an alternative is that we also don't allow using FOR PORTION OF 
together with RETURNING?)

> working on a keyword. But I think we could add it later.

Yeah, this seems like something we could change later with relative 
ease.  Maybe solicit some input from the public during beta?

> address them before merging the main patch.

It's still hard to understand.  I would be ok in general to say that 
results might be unexpected unless you use REPEATABLE READ.  Especially 
as it seems that a technical solution to improve this would be possible 
later.  But we should document this in more detail.  The verbal 
explanations are hard to interpret.  Could you maybe come up with a 
couple of ASCII-art flow charts that explains how things could go 
strange that we could put into the documentation?

Could we actually put some of these strange/unexpected behaviors into 
the isolation tests?  Right now we only test that the workaround works 
but not the initial problem.  Is this possible?  (Would we need 
injection points?)

Could we cut back the isolation tests a bit? They are the second slowest 
isolation test now, and the second largest expected file.  Maybe we 
don't need to test SERIALIZE separately?  (Assume that SERIALIZE is as 
good as or better than REPEATABLE READ?)

Attached is a patch with a few small cosmetic corrections.  In 
ExecForPortionOfLeftovers(), the comment and code that I delete is 
duplicated before and inside the loop.  The one before the
loop is probably sufficient.

Other than all that, this patch set (0001 through 0003) seems good to me.
From eb73e1971d0fd05b6d66c3b79552b396fc5f3b22 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 25 Mar 2026 16:42:18 +0100
Subject: [PATCH] Fixups

---
 src/backend/executor/nodeModifyTable.c       |  9 +--------
 src/backend/parser/analyze.c                 |  3 +--
 src/include/nodes/execnodes.h                |  1 +
 src/test/regress/expected/for_portion_of.out | 18 +++++++++---------
 src/test/regress/sql/for_portion_of.sql      | 18 +++++++++---------
 5 files changed, 21 insertions(+), 28 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index e16299cc2ed..9324bb1a093 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -135,6 +135,7 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
+
 static void ExecBatchInsert(ModifyTableState *mtstate,
 							ResultRelInfo *resultRelInfo,
 							TupleTableSlot **slots,
@@ -1588,14 +1589,6 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 		leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
 		ExecMaterializeSlot(leftoverSlot);
 
-		/*
-		 * If there are partitions, we must insert into the root table, so we
-		 * get tuple routing. We already set up leftoverSlot with the root
-		 * tuple descriptor.
-		 */
-		if (resultRelInfo->ri_RootResultRelInfo)
-			resultRelInfo = resultRelInfo->ri_RootResultRelInfo;
-
 		/*
 		 * The standard says that each temporal leftover should execute its
 		 * own INSERT statement, firing all statement and row triggers, but
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index f0e9ebaddd5..9e9b7fb18d2 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1348,8 +1348,7 @@ transformForPortionOfClause(ParseState *pstate,
 
 	attbasetype = getBaseType(attr->atttypid);
 
-	rangeVar = makeVar(
-					   rtindex,
+	rangeVar = makeVar(rtindex,
 					   range_attno,
 					   attr->atttypid,
 					   attr->atttypmod,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 177ded4e5cd..090cfccf65f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -41,6 +41,7 @@
 #include "utils/reltrigger.h"
 #include "utils/typcache.h"
 
+
 /*
  * forward references in this file
  */
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 8a29a19f501..31f772c723d 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -1636,17 +1636,17 @@ CREATE TABLE for_portion_of_test (
 INSERT INTO for_portion_of_test (id, valid_at, name) VALUES
   ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
 CREATE CONSTRAINT TRIGGER fpo_after_insert_row
-AFTER INSERT ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 CREATE CONSTRAINT TRIGGER fpo_after_update_row
-AFTER UPDATE ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 CREATE CONSTRAINT TRIGGER fpo_after_delete_row
-AFTER DELETE ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 BEGIN;
 UPDATE for_portion_of_test
   FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index 20d7e879c14..d4062acf1d1 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1037,19 +1037,19 @@ CREATE TABLE for_portion_of_test (
   ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
 
 CREATE CONSTRAINT TRIGGER fpo_after_insert_row
-AFTER INSERT ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER INSERT ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 
 CREATE CONSTRAINT TRIGGER fpo_after_update_row
-AFTER UPDATE ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER UPDATE ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 
 CREATE CONSTRAINT TRIGGER fpo_after_delete_row
-AFTER DELETE ON for_portion_of_test
-DEFERRABLE INITIALLY DEFERRED
-FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
+  AFTER DELETE ON for_portion_of_test
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false);
 
 BEGIN;
2.53.0

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 27 Mar 2026 14:38:32 -0700

Updated patches attached.

> together with RETURNING?)

Okay, thanks! I think it's worth supporting RETURNING, and this
behavior seems like the most useful.

> ease.  Maybe solicit some input from the public during beta?

That sounds good to me. Also if we did want to forbid nulls here, we
could choose to do it when valid_at is a PERIOD (once that lands) but
permit them when it's a range column. That seems like it best matches
the expected behavior in both scenarios. In fact, if the PERIOD's
start/end columns are NOT NULL, then we are already enforcing the rule
indirectly.

> strange that we could put into the documentation?

I made a sequence diagram and rewrote that section of the docs. I
think it's much clearer now.

> but not the initial problem.  Is this possible?  (Would we need

That's a good idea; I should have done it before. Done. No injection
points needed.

> good as or better than REPEATABLE READ?)

I removed all but a couple of the SERIALIZABLE tests.

> loop is probably sufficient.

Applied.

> Other than all that, this patch set (0001 through 0003) seems good to me.

Thanks! These v70 patches are rebased onto f39cb8c011.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From 1daeadc2cef953a2e39ca3525e5d53c8e6ca8fdf Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 15:40:06 -0700
Subject: [PATCH v70 4/7] Add tg_temporal to TriggerData

 
 
index 6596843a8d8..499615191ca 100644

 

 

+/* ----------
+ * ForPortionOfState()
+ *
 
index 27af5284406..c82ced079f8 100644
 
 /*
-- 
2.47.3

From 039f4dd239dfc3e1150e2bc6d5931b3a36a855e6 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 31 Oct 2025 19:59:52 -0700
Subject: [PATCH v70 3/7] Add isolation tests for UPDATE/DELETE FOR PORTION OF

Author: Paul A. Jungwirth <[email-masked]>
---
 doc/src/sgml/dml.sgml                         |   38 +
 doc/src/sgml/images/Makefile                  |    3 +-
 doc/src/sgml/images/meson.build               |    1 +
 doc/src/sgml/images/temporal-isolation.svg    |   47 +
 doc/src/sgml/images/temporal-isolation.txt    |   44 +
 src/backend/executor/nodeModifyTable.c        |    4 +
 .../isolation/expected/for-portion-of.out     | 4089 +++++++++++++++++
 src/test/isolation/isolation_schedule         |    1 +
 src/test/isolation/specs/for-portion-of.spec  |  597 +++
 9 files changed, 4823 insertions(+), 1 deletion(-)
 create mode 100644 doc/src/sgml/images/temporal-isolation.svg
 create mode 100644 doc/src/sgml/images/temporal-isolation.txt
 create mode 100644 src/test/isolation/expected/for-portion-of.out
 create mode 100644 src/test/isolation/specs/for-portion-of.spec

diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
index 08c0e759719..b1c56fb7e73 100644
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -393,6 +393,44 @@ WHERE product_no = 5;
    column references are not.
   </para>
 
+  <para>
+   In <literal>READ COMMITTED</literal> mode, temporal updates and deletes can
+   yield unexpected results when they concurrently touch the same row. It is
+   possible to lose all or part of the second update or delete. The scenario is
+   illustrated in <xref linkend="temporal-isolation-figure"/>. Session 2 searches
+   for rows to change, and it finds one that Session 1 has already modified.
+   It waits for Session 1 to commit. Then it re-checks whether the row still
+   matches its search criteria (including the start/end times targeted by
+   <literal>FOR PORTION OF</literal>). Session 1 may have changed those times
+   so that they no longer qualify.
+  </para>
+
+  <para>
+   In addition, the temporal leftovers inserted by Session 1 are not visible
+   within Session 2's transaction, because they are not yet committed.
+   Therefore there is nothing for Session 2 to update/delete: neither the
+   modified row nor the leftovers. The portion of history that Session 2
+   intended to change is not affected.
+  </para>
+
+  <figure id="temporal-isolation-figure">
+   <title>Temporal Isolation Example</title>
+   <mediaobject>
+    <imageobject>
+     <imagedata fileref="images/temporal-isolation.svg" format="SVG" width="35%"/>
+    </imageobject>
+   </mediaobject>
+  </figure>
+
+  <para>
+   To solve these
+   problems, precede every temporal update/delete with a <literal>SELECT FOR
+   UPDATE</literal> matching the same criteria (including the targeted portion of
+   application time). That way the actual update/delete doesn't begin until the
+   lock is held, and all concurrent leftovers will be visible. In other
+   transaction isolation levels, this lock is not required.
+  </para>
+
   <para>
    When temporal leftovers are inserted, all <literal>INSERT</literal>
    triggers are fired, but permission checks for inserting rows are
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index 38f8869d78d..157c1ff366a 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -9,7 +9,8 @@ ALL_IMAGES = \
 	temporal-entities.svg \
 	temporal-references.svg \
 	temporal-update.svg \
-	temporal-delete.svg
+	temporal-delete.svg \
+	temporal-isolation.svg
 
 DITAA = ditaa
 DOT = dot
diff --git a/doc/src/sgml/images/meson.build b/doc/src/sgml/images/meson.build
index ea1fdee3901..220e3eaafb8 100644
--- a/doc/src/sgml/images/meson.build
+++ b/doc/src/sgml/images/meson.build
@@ -18,6 +18,7 @@ all_files = [
   'temporal-references.txt',
   'temporal-update.txt',
   'temporal-delete.txt',
+  'temporal-isolation.txt',
 ]
 
 foreach file : all_files
diff --git a/doc/src/sgml/images/temporal-isolation.svg b/doc/src/sgml/images/temporal-isolation.svg
new file mode 100644
index 00000000000..6f88c763800
--- /dev/null
+++ b/doc/src/sgml/images/temporal-isolation.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 672" width="350" height="672" shape-rendering="geometricPrecision" version="1.0">
+  <defs>
+    <rect x="0" y="0" width="350" height="672" style="fill: #ffffff"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#ffff33" d="M205.0 301.0 L205.0 371.0 L325.0 371.0 L325.0 301.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#ffff33" d="M325.0 469.0 L325.0 539.0 L205.0 539.0 L205.0 469.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M25.0 35.0 L25.0 91.0 L145.0 91.0 L145.0 35.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M25.0 133.0 L25.0 189.0 L145.0 189.0 L145.0 133.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M25.0 287.0 L145.0 287.0 L145.0 231.0 L25.0 231.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#ffff33" d="M205.0 133.0 L205.0 189.0 L325.0 189.0 L325.0 133.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#ffff33" d="M325.0 581.0 L205.0 581.0 L205.0 637.0 L325.0 637.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#ffff33" d="M205.0 91.0 L325.0 91.0 L325.0 35.0 L205.0 35.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#99dd99" d="M145.0 399.0 L145.0 455.0 L25.0 455.0 L25.0 399.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M80.0 112.0 L85.0 126.0 L90.0 112.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M260.0 112.0 L265.0 126.0 L270.0 112.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M80.0 210.0 L85.0 224.0 L90.0 210.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M260.0 280.0 L265.0 294.0 L270.0 280.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M80.0 378.0 L85.0 392.0 L90.0 378.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M260.0 448.0 L265.0 462.0 L270.0 448.0 z"/>
+    <path stroke="none" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="#000000" d="M260.0 560.0 L265.0 574.0 L270.0 560.0 z"/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M85.0 91.0 L85.0 119.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M85.0 189.0 L85.0 217.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M265.0 539.0 L265.0 567.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M265.0 91.0 L265.0 119.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M265.0 371.0 L265.0 455.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M85.0 385.0 L85.0 287.0 "/>
+    <path stroke="#000000" stroke-width="1.000000" stroke-linecap="round" stroke-linejoin="round" fill="none" d="M265.0 189.0 L265.0 287.0 "/>
+    <text x="46" y="68" font-family="Courier" font-size="13" stroke="none" fill="#000000">Session 1</text>
+    <text x="220" y="334" font-family="Courier" font-size="13" stroke="none" fill="#000000">UPDATE;</text>
+    <text x="220" y="348" font-family="Courier" font-size="13" stroke="none" fill="#000000">waits...</text>
+    <text x="48" y="166" font-family="Courier" font-size="13" stroke="none" fill="#000000">BEGIN;</text>
+    <text x="228" y="166" font-family="Courier" font-size="13" stroke="none" fill="#000000">BEGIN;</text>
+    <text x="226" y="68" font-family="Courier" font-size="13" stroke="none" fill="#000000">Session 2</text>
+    <text x="47" y="264" font-family="Courier" font-size="13" stroke="none" fill="#000000">UPDATE;</text>
+    <text x="46" y="432" font-family="Courier" font-size="13" stroke="none" fill="#000000">COMMIT;</text>
+    <text x="226" y="614" font-family="Courier" font-size="13" stroke="none" fill="#000000">COMMIT;</text>
+    <text x="220" y="502" font-family="Courier" font-size="13" stroke="none" fill="#000000">...resumes</text>
+    <text x="220" y="516" font-family="Courier" font-size="13" stroke="none" fill="#000000">rechecks</text>
diff --git a/doc/src/sgml/images/temporal-isolation.txt b/doc/src/sgml/images/temporal-isolation.txt
index 00000000000..a65d81abdbb
+++ b/doc/src/sgml/images/temporal-isolation.txt
@@ -0,0 +1,44 @@
++-----------+     +-----------+
+| cGRE      |     | cYEL      |
+|Session 1  |     |Session 2  |
+|           |     |           |
++-----+-----+     +-----+-----+
+      v                 v
++-----+-----+     +-----+-----+
+| cGRE      |     | cYEL      |
+| BEGIN;    |     | BEGIN;    |
+|           |     |           |
++-----+-----+     +-----+-----+
+      v                 |
++-----+-----+           |
+| cGRE      |           |
+| UPDATE;   |           |
+|           |           |
++-----+-----+           v
+      |           +-----+-----+
+      |           | cYEL      |
+      |           | UPDATE;   |
+      |           | waits...  |
+      |           |           |
+      |           +-----+-----+
+      v                 |
++-----+-----+           |
+| cGRE      |           |
+| COMMIT;   |           |
+|           |           |
++-----------+           v
+                  +-----+-----+
+                  | cYEL      |
+                  | ...resumes|
+                  | rechecks  |
+                  +-----+-----+
+                        |
+                        v
+                  +-----+-----+
+                  | cYEL      |
+                  | COMMIT;   |
+                  +-----------+
index f50610e25e3..9324bb1a093 100644
@@ -1465,6 +1465,10 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
index 00000000000..2438c469599
@@ -0,0 +1,4089 @@
+starting permutation: s1rc s2rc s2upd2027 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2upd202503 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2upd20252026 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del2027 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del202503 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del20252026 s1upd2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2upd2027 s1del2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2upd202503 s1del2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2upd20252026 s1del2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del2027 s1del2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del202503 s1del2025 s2c s1c s1q
+starting permutation: s1rc s2rc s2del20252026 s1del2025 s2c s1c s1q
index 00000000000..1ab9b257551
@@ -0,0 +1,597 @@
+# demonstrate the problem and also show that solving it with manual locks is
+# viable and not vitiated by any bugs. Incidentally, this approach also works in
+# MariaDB.
+# We run the same tests under REPEATABLE READ to show the problem goes away.
+# To save on test time, we only run a couple SERIALIZABLE tests (for the more
+# problematic permutations).
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be updated either.
+permutation s1rc s2rc s2upd2027 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# but EvalPlanQual still matches the row to be updated.
+permutation s1rc s2rc s2upd202503 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# but EvalPlanQual still matches the row to be updated,
+# and s1's leftovers don't conflict with s2's.
+permutation s1rc s2rc s2upd20252026 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be updated either.
+permutation s1rc s2rc s2del2027 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be updated either.
+permutation s1rc s2rc s2del202503 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be updated either.
+permutation s1rc s2rc s2del20252026 s1upd2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be deleted either.
+permutation s1rc s2rc s2upd2027 s1del2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# but EvalPlanQual still matches the row to be deleted.
+permutation s1rc s2rc s2upd202503 s1del2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# but EvalPlanQual still matches the row to be deleted,
+# and s1 leaves leftovers from the row created by s2.
+permutation s1rc s2rc s2upd20252026 s1del2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be deleted either.
+permutation s1rc s2rc s2del2027 s1del2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be deleted either.
+permutation s1rc s2rc s2del202503 s1del2025 s2c s1c s1q
+# Workaround:
+# Problem:
+# s1 (without locking) overlooks the leftovers from s2
+# and EvalPlanQual no longer matches the row to be deleted either.
+permutation s1rc s2rc s2del20252026 s1del2025 s2c s1c s1q
+# Workaround:

From 8d001f1a1946eb91de04e5124352b5823b2b412c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 13 Jun 2025 16:11:47 -0700
Subject: [PATCH v70 5/7] Look up additional temporal foreign key helper proc

 
 /*
- * FindFKPeriodOpers -
 
+	/*
 
 /*
index c69c12dc014..4fc5ffd87b3 100644

index 9e9b7fb18d2..db1d5079c98 100644
@@ -1545,7 +1545,7 @@ transformForPortionOfClause(ParseState *pstate,

 
 	ReleaseSysCache(tup);

From ee09e8c09935744b6c73d5701e9e5bd10d1a7d47 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v70 2/7] Add UPDATE/DELETE FOR PORTION OF

 doc/src/sgml/images/meson.build               |    2 +
 src/backend/executor/nodeModifyTable.c        |  349 ++-
 src/backend/parser/analyze.c                  |  358 ++-
 51 files changed, 5699 insertions(+), 88 deletions(-)

 

 INSERT INTO "S 1"."T 1"
 	SELECT id,
 
 ANALYZE "S 1"."T 1";
 ANALYZE "S 1"."T 2";
 ANALYZE "S 1"."T 3";
 ANALYZE "S 1"."T 4";
+ANALYZE "S 1"."T 5";

 
 
+-- FOR PORTION OF setup
 
+-- FOR PORTION OF setup

 
 
 DITAA = ditaa
 DOT = dot
diff --git a/doc/src/sgml/images/meson.build b/doc/src/sgml/images/meson.build
index 8e601e877a5..ea1fdee3901 100644
--- a/doc/src/sgml/images/meson.build
+++ b/doc/src/sgml/images/meson.build
@@ -16,6 +16,8 @@ all_files = [
   'pagelayout.txt',
   'temporal-entities.txt',
   'temporal-references.txt',
+  'temporal-update.txt',
+  'temporal-delete.txt',
 ]
 
 foreach file : all_files
 
+  <para>

+  <para>
 
+   <varlistentry>
 
   <para>

 
+  <para>
 
+   <varlistentry>
 
   <para>

+   <para>
index 4cd5e262e0f..f50610e25e3 100644
 
@@ -167,6 +168,10 @@ static bool ExecOnConflictSelect(ModifyTableContext *context,
@@ -189,6 +194,9 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,

 /*
@@ -1384,6 +1392,227 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
@@ -1537,7 +1766,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1570,6 +1800,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1995,7 +2229,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2344,7 +2581,8 @@ lreplace:
@@ -2366,6 +2604,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL);
 	}
 
@@ -5298,6 +5540,107 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*

index d19800ad6a5..757f212d3ac 100644
@@ -2209,6 +2209,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,

index ad31dee2686..9e9b7fb18d2 100644
 
 #include "postgres.h"

 

 		/* Process the SELECT/UPDATE WHERE clause */
 		onConflictWhere = transformWhereClause(pstate,
@@ -1279,6 +1295,320 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
 
 /*
@@ -2538,6 +2868,13 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2564,7 +2901,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2583,7 +2921,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2636,6 +2974,20 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 0fea726cdd5..c7e31f11fee 100644

 
@@ -13200,6 +13205,21 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 
 using_clause:
@@ -13274,6 +13294,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
 
 set_clause_list:
@@ -14787,6 +14826,55 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+/*
@@ -15627,16 +15715,25 @@ opt_timezone:
 
+/*
@@ -18934,6 +19031,7 @@ unreserved_keyword:
@@ -19578,6 +19676,7 @@ bare_label_keyword:

 			/*
index 312dfdc182a..f5283976999 100644
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 

 
+/*
index 684e398f824..090cfccf65f 100644
@@ -39,6 +39,7 @@

 /*
@@ -464,6 +465,24 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -600,6 +619,9 @@ typedef struct ResultRelInfo

index df431220ac5..2f9bc111d2d 100644

+/*
+ * ForPortionOfClause

index 7947d83d584..693b879f76d 100644
@@ -2721,6 +2721,7 @@ typedef struct ModifyTablePath
index f5b6b45664a..2517dd4af94 100644
@@ -2417,4 +2417,39 @@ typedef struct OnConflictExpr
 
+/*----------
index 00000000000..31f772c723d
index 7069e9febb8..0de13612818 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 
index 734da057c34..3a044ffd8bf 100644
 
 # ----------
index 00000000000..d4062acf1d1
index 9f21c2945bd..95a46854b37 100644

 

 
 -- temporal UNIQUE:

 

 

 

 

 

 

 

 

 

 

 

 # cleanup

 

 

 
 
 # cleanup

 

 

 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 112653c1680..cde0ed1e175 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -863,6 +863,9 @@ ForBothState
 ForEachState
 ForFiveState
 ForFourState
+ForPortionOfClause
+ForPortionOfExpr
+ForPortionOfState
 ForThreeState
 ForeignAsyncConfigureWait_function
 ForeignAsyncNotify_function
-- 
2.47.3

From a3a438c4db58697977512bb34788e710a3b157e0 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 2 Dec 2025 21:30:13 -0800
Subject: [PATCH v70 1/7] Add range_get_constructor2 to lsyscache

 
+/*

From 7ac2ebbfcb2596bcdfe2c11a580d13f2f5cdab59 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v70 7/7] Expose FOR PORTION OF to plpgsql triggers

index 832d65e97ca..d664201498d 100644
@@ -1598,7 +1598,7 @@ SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements

 
 		case PLPGSQL_EVENT_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 65b0fd0790f..158e823a6b0 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -1416,6 +1416,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 						PLpgSQL_var *var)
 {
 	MemoryContext oldcontext;
+	ForPortionOfState *fpo;
 
 	if (var->promise == PLPGSQL_PROMISE_NONE)
 		return;					/* nothing to do */
@@ -1547,6 +1548,37 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 			}
 			break;

 /*
index 31f772c723d..8c0e9565139 100644
 
   IF TG_ARGV[0] THEN
index d4062acf1d1..347f78da87f 100644
 
   IF TG_ARGV[0] THEN

From 6d2a7b0102a80e8fe5c55532e270dc8d6de78d14 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v70 6/7] Add CASCADE/SET NULL/SET DEFAULT for temporal foreign
 keys

 
     <para>
index 80829b23945..a90ca8ccdce 100644
@@ -1336,7 +1336,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1351,7 +1353,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1368,7 +1373,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
index 4fc5ffd87b3..9d475503c7a 100644

 

 /*

+/*
 
 	/*
 
 	/*

 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index 0118e970dda..53cad17c3aa 100644
@@ -4143,6 +4143,28 @@

 
 explain (costs off)

 explain (costs off)
 
+--
 
+--
 
+--

+--
+
+--

 

 

 

 

 

 

 
-- 
2.47.3

---

From: jian he <[email-masked]>
Date: Tue, 7 Apr 2026 12:03:52 +0800

hi.
https://git.postgresql.org/cgit/postgresql.git/commit/?id=8e72d914c52876525a90b28444453de8085c866f

DROP TABLE If EXISTS tt;
CREATE TABLE tt(id int, valid_at int4range, amt int, CONSTRAINT
fpo2_check CHECK (upper(valid_at) <> '11'));
CREATE OR REPLACE FUNCTION dummy_update_func() RETURNS trigger AS $$
BEGIN
  RAISE NOTICE 'dummy_update_func(%) called: action = %, old = %, new = %',
    TG_ARGV[0], TG_OP, OLD, NEW;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER some_trig_before BEFORE UPDATE OR INSERT ON tt FOR EACH
ROW EXECUTE PROCEDURE dummy_update_func('before');
INSERT INTO tt VALUES (1, '[1,100)', 2);
UPDATE tt FOR PORTION OF valid_at FROM 1 TO 12 SET amt = 3;

NOTICE:  dummy_update_func(before) called: action = UPDATE, old =
(1,"[1,100)",2), new = (1,"[1,12)",3)
NOTICE:  dummy_update_func(before) called: action = INSERT, old =
<NULL>, new = (1,"[12,100)",2)

As you can see, ExecGetAllUpdatedCols does not account for the valid_at column,
even though it is actively being updated. ExecGetAllUpdatedCols is being used
serval places, IMHO, we need to add some comments on
ExecGetAllUpdatedCols explaining
this behavior and maybe add some regression tests.

I'm not sure if it's safe for ExecGetAllUpdatedCols to ignore the FOR
PORTION OF column.

I reliazed this issue because of https://commitfest.postgresql.org/patch/6270/
I saw your transformForPortionOfClause comments.
        /*
         * The range column will change, but you don't need UPDATE permission
         * on it, so we don't add to updatedCols here. XXX: If
         * https://www.postgresql.org/message-id/CACJufxEtY1hdLcx%3DFhnqp-ERcV1PhbvELG5COy_CZjoEW76ZPQ%40mail.gmail.com
         * is merged (only validate CHECK constraints if they depend on one of
         * the columns being UPDATEd), we need to make sure that code knows
         * that we are updating the application-time column.
         */
But this comment is about FOR PORTION OF column permission, not about
ExecGetAllUpdatedCols.

-------------------------------------------------------------------------------------------------------------------
transformForPortionOfClause
    if (contain_volatile_functions_after_planning((Expr *) result->targetRange))
        ereport(ERROR,
                (errmsg("FOR PORTION OF bounds cannot contain volatile
functions")));

Need
errcode(ERRCODE_FEATURE_NOT_SUPPORTED).

coerce_to_target_type function comment:
 * This is the general-purpose entry point for arbitrary type coercion
 * operations.  Direct use of the component operations can_coerce_type,
 * coerce_type, and coerce_type_typmod should be restricted to special
 * cases (eg, when the conversion is expected to succeed).

We should use coerce_to_target_type more, not can_coerce_type,
coerce_type individually.
coerce_to_target_type also handles `UNKNOWN` constant, which ensures
the deparsing casts to the correct data type.

please see the attached refactoring for
https://git.postgresql.org/cgit/postgresql.git/commit/?id=8e72d914c52876525a90b28444453de8085c866f

--
jian
https://www.enterprisedb.com/

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 7 Apr 2026 13:53:13 +0200

> Thanks! These v70 patches are rebased onto f39cb8c011.

I have committed the patches 0001 through 0003.  (I did some editing on 
the documentation.)  I think this is about as far as we can go for this 
release.

Please check the follow-up bug report(?) posted in this thread.

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 7 Apr 2026 07:32:40 -0700

Hi Peter, Paul,

Please see a few bug reports related to this at [1], [2], [3].
Additionally, it appears there is another issue here:

 A BEFORE UPDATE trigger that modifies the range column creates overlapping
rows. The trigger widening the range doesn't affect leftover computation,
which uses the original FPO bounds. Result: updated row overlaps both
leftovers.

CREATE TABLE fpo_trigger_overlap (
    id int,
    valid_at daterange,
    val text
);

-- BEFORE UPDATE trigger that resets the range to the full year
CREATE FUNCTION widen_range() RETURNS trigger AS $$
BEGIN
    NEW.valid_at := daterange('2024-01-01', '2025-01-01');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_widen BEFORE UPDATE ON fpo_trigger_overlap
    FOR EACH ROW EXECUTE FUNCTION widen_range();

INSERT INTO fpo_trigger_overlap
    VALUES (1, '[2024-01-01, 2025-01-01)', 'original');

UPDATE fpo_trigger_overlap
    FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-09-01'
    SET val = 'modified';

-- Detect overlaps (should be 0 rows for correct behavior):
SELECT a.valid_at AS range_a, a.val AS val_a,
       b.valid_at AS range_b, b.val AS val_b
FROM fpo_trigger_overlap a, fpo_trigger_overlap b
WHERE a.ctid < b.ctid AND a.valid_at && b.valid_at;

-- cleanup
DROP TABLE fpo_trigger_overlap;
DROP FUNCTION widen_range();

[1]
https://www.postgresql.org/message-id/CAHg%2BQDcd%3Dt69gLf9yQexO07EJ2mx0Z70NFHo6h94X1EDA%3DhM0g%40mail.gmail.com

[2]
https://www.postgresql.org/message-id/CAHg%2BQDcsXsUVaZ%2BJwM02yDRQEi%3DcL_rTH_ROLDYgOx004sQu7A%40mail.gmail.com

[3]
https://www.postgresql.org/message-id/CANE55rCqcse_pwXBMWhbj3_7XROb8Dks6%3DOLFmKy3bO3zDsCsg%40mail.gmail.com

Thanks,
Satya

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 9 Apr 2026 12:35:13 -0700

Hi Paul, Peter,

I found a Server crash when using UPDATE ... FOR PORTION OF or DELETE ...
FOR PORTION OF on a view that has INSTEAD OF triggers.

Repro:

CREATE TABLE t (id INT, valid_at daterange, val INT);
INSERT INTO t VALUES (1, '[2026-01-01,2026-12-31)', 100);
CREATE VIEW v AS SELECT * FROM t;

CREATE FUNCTION v_trig() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
    UPDATE t SET val = NEW.val WHERE id = OLD.id;
    RETURN NEW;
END;
$$;
CREATE TRIGGER trg INSTEAD OF UPDATE ON v
    FOR EACH ROW EXECUTE FUNCTION v_trig();

-- This crashes the server:
UPDATE v FOR PORTION OF valid_at FROM '2026-04-01' TO '2026-08-01'
    SET val = 999 WHERE id = 1;

I am thinking we should just reject this case. Attached a draft patch to
fix the issue.

Thanks,
Satya

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 9 Apr 2026 12:42:23 -0700

> I am thinking we should just reject this case. Attached a draft patch to

Patches attached now.

---

From: jian he <[email-masked]>
Date: Tue, 14 Apr 2026 12:33:07 +0800

On Fri, Apr 10, 2026 at 3:42 AM SATYANARAYANA NARLAPURAM
<[email-masked]> wrote:
>> I am thinking we should just reject this case. Attached a draft patch to fix the issue.
>
Yech, we should reject it.

In RewriteQuery, we have:
/*
 * If there was no unqualified INSTEAD rule, and the target relation
 * is a view without any INSTEAD OF triggers, see if the view can be
 * automatically updated.  If so, we perform the necessary query
 * transformation here and add the resulting query to the
 * product_queries list, so that it gets recursively rewritten if
 * necessary.  For MERGE, the view must be automatically updatable if
 * any of the merge actions lack a corresponding INSTEAD OF trigger.
 *
 * If the view cannot be automatically updated, we throw an error here
 * which is OK since the query would fail at runtime anyway.  Throwing
 * the error here is preferable to the executor check since we have
 * more detailed information available about why the view isn't
 * updatable.
 */
if (!instead &&
    rt_entry_relation->rd_rel->relkind == RELKIND_VIEW &&
    !view_has_instead_trigger(rt_entry_relation, event,
                            parsetree->mergeActionList))

Per above, RewriteQuery does not rewrite the view relation to its base
relation when the view has an INSTEAD OF trigger.
In such cases, ExecInitModifyTable->ExecInitResultRelation initialize
mtstate->resultRelInfo
using the view relation itself (rather than the underlying base table).
But ExecForPortionOfLeftovers->table_tuple_fetch_row_version requires the
relation to physical storage.

Therefore DELETE/UPDATE ... FOR PORTION OF operations cannot cope with
views that have INSTEAD OF triggers.
IMHO, rejecting it at  RewriteQuery make more sense to me.

Now the error message is:
ERROR:  UPDATE FOR PORTION OF is not supported for views with INSTEAD
OF triggers
ERROR:  DELETE FOR PORTION OF is not supported for views with INSTEAD
OF triggers

--
jian
https://www.enterprisedb.com/
From f37b8d41c211d8f05b3fe7b96a0bf618890e7170 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 14 Apr 2026 12:10:09 +0800
Subject: [PATCH v10 1/1] reject instead of view with DELETE/UPDATE FOR PORTION
 OF

Views with INSTEAD OF triggers cannot rewrite the query to the base relation.
Views do not have physical rows.  FOR PORTION OF requires access to the physical
row to compute temporal leftovers, so we must disallow it here.

discussion: https://postgr.es/m/CAHg%2BQDd74fnd4obCRMqVS0AVWf%3DcSFH%3DCv7trTJWgm%2B_bhTK6w%40mail.gmail.com
commitfest entry: https://commitfest.postgresql.org/patch/
---
 src/backend/rewrite/rewriteHandler.c         | 28 ++++++++++++++++++++
 src/test/regress/expected/for_portion_of.out | 21 +++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 23 ++++++++++++++++
 3 files changed, 72 insertions(+)

diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 021c73f1b67..dbfbfcaf34a 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -4166,6 +4166,20 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 						parsetree->targetList = lappend(parsetree->targetList, tle);
 					}
 				}
+				else if (view_has_instead_trigger(rt_entry_relation, event, NIL))
+				{
+					/*
+					 * Views with INSTEAD OF triggers cannot rewrite the query
+					 * to the base relation. Views do not have physical rows.
+					 * FOR PORTION OF requires access to the physical row to
+					 * compute temporal leftovers, so we must disallow it
+					 * here.
+					 */
+					if (parsetree->forPortionOf)
+						ereport(ERROR,
+								errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								errmsg("UPDATE FOR PORTION OF is not supported for views with INSTEAD OF triggers"));
+				}
 			}
 
 			parsetree->targetList =
@@ -4231,6 +4245,20 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
 					 */
 					AddQual(parsetree, parsetree->forPortionOf->overlapsExpr);
 				}
+				else if (view_has_instead_trigger(rt_entry_relation, event, NIL))
+				{
+					/*
+					 * Views with INSTEAD OF triggers cannot rewrite the query
+					 * to the base relation. Views do not have physical rows.
+					 * FOR PORTION OF requires access to the physical row to
+					 * compute temporal leftovers, so we must disallow it
+					 * here.
+					 */
+					if (parsetree->forPortionOf)
+						ereport(ERROR,
+								errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+								errmsg("DELETE FOR PORTION OF is not supported for views with INSTEAD OF triggers"));
+				}
 			}
 		}
 		else
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 31f772c723d..9e3c337e0e0 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2097,4 +2097,25 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
 (4 rows)
 
 DROP TABLE temporal_partitioned;
+-- Test: FOR PORTION OF should be rejected on views with INSTEAD OF triggers
+CREATE TABLE fpo_instead_base (id int, valid_at daterange, val int);
+INSERT INTO fpo_instead_base VALUES (1, '[2024-01-01,2024-12-31)', 100);
+CREATE VIEW fpo_instead_view AS SELECT * FROM fpo_instead_base;
+CREATE FUNCTION fpo_instead_trig_fn() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+    RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_instead_trig INSTEAD OF UPDATE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+CREATE TRIGGER fpo_instead_del_trig INSTEAD OF DELETE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    SET val = 999 WHERE id = 1;  -- error
+ERROR:  UPDATE FOR PORTION OF is not supported for views with INSTEAD OF triggers
+DELETE FROM fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    WHERE id = 1;  -- error
+ERROR:  DELETE FOR PORTION OF is not supported for views with INSTEAD OF triggers
+DROP VIEW fpo_instead_view;
+DROP TABLE fpo_instead_base;
 RESET datestyle;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index d4062acf1d1..d4602171c90 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1365,4 +1365,27 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;

+-- Test: FOR PORTION OF should be rejected on views with INSTEAD OF triggers
+CREATE TABLE fpo_instead_base (id int, valid_at daterange, val int);
+INSERT INTO fpo_instead_base VALUES (1, '[2024-01-01,2024-12-31)', 100);
+CREATE VIEW fpo_instead_view AS SELECT * FROM fpo_instead_base;
+CREATE FUNCTION fpo_instead_trig_fn() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+    RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_instead_trig INSTEAD OF UPDATE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+CREATE TRIGGER fpo_instead_del_trig INSTEAD OF DELETE ON fpo_instead_view
+  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();
+
+UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    SET val = 999 WHERE id = 1;  -- error
+
+DELETE FROM fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO '2024-08-01'
+    WHERE id = 1;  -- error
+
+DROP VIEW fpo_instead_view;
+DROP TABLE fpo_instead_base;
+
 RESET datestyle;
-- 
2.34.1

---

From: Paul A Jungwirth <[email-masked]>
Date: Tue, 14 Apr 2026 22:34:11 -0700

> Please see a few bug reports related to this at [1], [2], [3].

Thanks for collecting all these bugs together and for already working
on patches for them! I've started going through them; I'll respond to
each thread individually.

For the bug here (widening the range in a BEFORE UPDATE trigger), see below:

>  A BEFORE UPDATE trigger that modifies the range column creates overlapping rows. The trigger widening the range doesn't affect leftover computation, which uses the original FPO bounds. Result: updated row overlaps both leftovers.
>

I'm working on a fix for this. It's not quite ready, but I can finish
it in the morning. . . .

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 15 Apr 2026 10:30:21 -0700

> I'm working on a fix for this. It's not quite ready, but I can finish

Actually I think the proper behavior here is to raise an error. We
forbid setting the application-time column when using FOR PORTION OF
(per the standard), so why should we allow a BEFORE trigger to set it?
I think it has the same inconsistency problems. We could support it,
but then why not support both?

Assuming we want to raise an error, I think the best way is to check
the tuple in ExecForPortionOfLeftovers to see if a trigger has
modified it, and in that case raise an error. What do you think?

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 15 Apr 2026 14:59:21 -0700

> I'm not sure if it's safe for ExecGetAllUpdatedCols to ignore the FOR

The other threads have found a couple problems with that now. I wonder
if we should have ExecGetExtraUpdatedCols add the application-time
attno to the returned bitmapset? Or even add it to updatedCols in
analysis and then ignore it for permission checking. That seems more
robust than finding all the places we need to add it, except
updatedCols is in a struct called RTEPermissionInfo. Best of all I
think would be to add a new bitmapset somewhere else and not use
permissions infrastructure for GENERATED columns, UPDATE OF triggers,
skipping CHECK constraints, etc. But is it too late in the cycle to
make a change like that?

In the short term, what about just doing this?:

@@ -1449,6 +1449,7 @@ ExecGetAllUpdatedCols(ResultRelInfo *relinfo,
EState *estate)
     oldcxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate));

     ret = bms_union(ExecGetUpdatedCols(relinfo, estate),
+                    ExecGetForPortionOfCol(relinfo, estate),
                     ExecGetExtraUpdatedCols(relinfo, estate));

     MemoryContextSwitchTo(oldcxt);

(Implementing that function is left as an exercise for the reader.)

> errcode(ERRCODE_FEATURE_NOT_SUPPORTED).

Okay.

> the deparsing casts to the correct data type.

Including the casts when we deparse does seem like an improvement.

The patch looks good to me.

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 15 Apr 2026 16:25:58 -0700

> Yech, we should reject it.

I think using INSTEAD OF triggers to replace an UPDATE/DELETE FOR
PORTION OF is a valid use-case, but it doesn't make sense to insert
temporal leftovers. As you say, we can't access the underlying
storage. But also we don't know what changes the trigger actually
made. The trigger should be responsible for leftovers, and we
shouldn't try to add more. So I think the fix is just to skip
inserting leftovers. I've attached a patch to do that.

This is a good use-case for a pending followup patch (which will have
to wait for v20 I think), which makes the FOR PORTION OF parameters
accessible to triggers. We need that ourselves for PERIOD foreign keys
with CASCADE/SET NULL/SET DEFAULT, but it's nice to have another
example of why you might want it.

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 18 Apr 2026 16:18:13 -0700

> modified it, and in that case raise an error. What do you think?

Here is a patch that forbids changing the valid_at column in a BEFORE
trigger. It works by capturing the value before triggers run, then
checking afterwards if it is still the same (using the default btree
equality operator; probably a simple binary comparison is good
enough).

This copy+check only happens if the table has BEFORE UPDATE row
triggers, so there is no cost in most cases.

I'm raising ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION, which is what we
use when (basically) a trigger & UPDATE both change a row in a way
that leaves the user intent unclear. I think that's a very close fit
here, but you could argue we should use the same errcode as SETing
valid_at. That is ERRCODE_SYNTAX_ERROR. That strikes me as a
questionable choice, actually. Personally I think using different
errcodes is correct though.

In ExecForPortionOfSaveRange there is a lot of code duplication
copying the structure for child partitions, but I think we could cut
that by first adding jian he's helper function (ExecInitForPortionOf)
from another bugfix patch [1].

[1] https://www.postgresql.org/message-id/CA%2BrenyWD%2BXXifwswE74vhjooqbiVKu4qVhLvpMcUQBzrjVjT7A%40mail.gmail.com

Yours,

-- 
Paul              ~{:-)
[email-masked]
From d1f93cbc5018c41b0948ece5eade08583afe6ae3 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Thu, 16 Apr 2026 13:00:59 -0700
Subject: [PATCH v1] Forbid BEFORE UPDATE triggers changing the FOR PORTION OF
 column

Just as we forbid UPDATE t FOR PORTION OF valid_at ... SET valid_at, we
should forbid setting the application-time column with a BEFORE trigger.

We record the value before triggers fire, and then we compare
afterwards to make sure it hasn't been altered. If so we raise an error.
---
 src/backend/executor/nodeModifyTable.c       | 159 +++++++++++++++++--
 src/include/nodes/execnodes.h                |   5 +
 src/test/regress/expected/for_portion_of.out |  20 +++
 src/test/regress/sql/for_portion_of.sql      |  24 +++
 4 files changed, 196 insertions(+), 12 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index ef2a6bc6e9d..c82dea2eff1 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -171,7 +171,11 @@ static bool ExecOnConflictSelect(ModifyTableContext *context,
 static void ExecForPortionOfLeftovers(ModifyTableContext *context,
 									  EState *estate,
 									  ResultRelInfo *resultRelInfo,
-									  ItemPointer tupleid);
+									  ItemPointer tupleid,
+									  TupleTableSlot *newSlot);
+static void ExecForPortionOfSaveRange(ModifyTableContext *context,
+									  ResultRelInfo *resultRelInfo,
+									  TupleTableSlot *slot);
 static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
 											   EState *estate,
 											   PartitionTupleRouting *proute,
@@ -1403,14 +1407,14 @@ static void
 ExecForPortionOfLeftovers(ModifyTableContext *context,
 						  EState *estate,
 						  ResultRelInfo *resultRelInfo,
-						  ItemPointer tupleid)
+						  ItemPointer tupleid,
+						  TupleTableSlot *newSlot)
 {
 	ModifyTableState *mtstate = context->mtstate;
 	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
 	ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
 	AttrNumber	rangeAttno;
 	Datum		oldRange;
-	TypeCacheEntry *typcache;
 	ForPortionOfState *fpoState;
 	TupleTableSlot *oldtupleSlot;
 	TupleTableSlot *leftoverSlot;
@@ -1490,15 +1494,51 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	oldRange = oldtupleSlot->tts_values[rangeAttno - 1];
 
 	/*
-	 * Get the range's type cache entry. This is worth caching for the whole
-	 * UPDATE/DELETE as range functions do.
+	 * If BEFORE UPDATE triggers fired, they might have changed the range
+	 * column, which would break the temporal semantics of FOR PORTION OF.
+	 * We captured the column value in ExecForPortionOfSaveRange, so now
+	 * compare it with the current value to detect tampering. This parallels
+	 * how in analysis we reject SETting the range column directly.
 	 */
-
-	typcache = fpoState->fp_leftoverstypcache;
-	if (typcache == NULL)
+	if (newSlot != NULL && fpoState->fp_origNewRangeValid)
 	{
-		typcache = lookup_type_cache(forPortionOf->rangeType, 0);
-		fpoState->fp_leftoverstypcache = typcache;
+		bool		newIsNull;
+		Datum		newRange;
+		TypeCacheEntry *typcache;
+
+		/*
+		 * Get the range's type cache entry. This is worth caching for the whole
+		 * UPDATE/DELETE as range functions do.
+		 */
+
+		typcache = fpoState->fp_leftoverstypcache;
+		if (typcache == NULL)
+		{
+			typcache = lookup_type_cache(forPortionOf->rangeType,
+										 TYPECACHE_EQ_OPR_FINFO);
+			fpoState->fp_leftoverstypcache = typcache;
+		}
+
+		slot_getallattrs(newSlot);
+		newIsNull = newSlot->tts_isnull[rangeAttno - 1];
+		newRange = newSlot->tts_values[rangeAttno - 1];
+
+		if (!OidIsValid(typcache->eq_opr_finfo.fn_oid))
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_FUNCTION),
+					errmsg("could not identify an equality operator for type %s",
+						   format_type_be(forPortionOf->rangeType)));
+
+		if (newIsNull != fpoState->fp_origNewRangeIsNull ||
+			(!newIsNull &&
+			 !DatumGetBool(FunctionCall2Coll(&typcache->eq_opr_finfo,
+											 InvalidOid,
+											 newRange,
+											 fpoState->fp_origNewRange))))
+			ereport(ERROR,
+					errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
+					errmsg("cannot change column \"%s\" from a BEFORE trigger because it is used in FOR PORTION OF",
+						   forPortionOf->range_name));
 	}
 
 	/*
@@ -1617,6 +1657,92 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	}
 }
 
+/* ----------------------------------------------------------------
+ *		ExecForPortionOfSaveRange
+ *
+ *		Capture the FOR PORTION OF range column value from the new tuple
+ *		slot just before BEFORE UPDATE triggers run. ExecForPortionOfLeftovers
+ *		later compares the saved value with the post-trigger value to detect
+ *		whether a trigger changed the range column, which is not allowed.
+ * ----------------------------------------------------------------
+ */
+static void
+ExecForPortionOfSaveRange(ModifyTableContext *context,
+						  ResultRelInfo *resultRelInfo,
+						  TupleTableSlot *slot)
+{
+	ModifyTableState *mtstate = context->mtstate;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+	ForPortionOfState *fpoState;
+	AttrNumber	rangeAttno;
+	TupleConversionMap *map = NULL;
+	MemoryContext oldcontext;
+
+	/*
+	 * Lazily initialize the partition child's ForPortionOfState, mirroring
+	 * ExecForPortionOfLeftovers so the saved value lives on the same struct
+	 * the check will read from.
+	 */
+	if (!resultRelInfo->ri_forPortionOf)
+	{
+		ForPortionOfState *leafState = makeNode(ForPortionOfState);
+		ForPortionOfState *rootFpoState;
+
+		if (!mtstate->rootResultRelInfo)
+			elog(ERROR, "no root relation but ri_forPortionOf is uninitialized");
+		rootFpoState = mtstate->rootResultRelInfo->ri_forPortionOf;
+		Assert(rootFpoState);
+
+		leafState->fp_rangeName = rootFpoState->fp_rangeName;
+		leafState->fp_rangeType = rootFpoState->fp_rangeType;
+		leafState->fp_rangeAttno = rootFpoState->fp_rangeAttno;
+		leafState->fp_targetRange = rootFpoState->fp_targetRange;
+		leafState->fp_Leftover = rootFpoState->fp_Leftover;
+		leafState->fp_Existing =
+			table_slot_create(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		resultRelInfo->ri_forPortionOf = leafState;
+	}
+	fpoState = resultRelInfo->ri_forPortionOf;
+
+	rangeAttno = forPortionOf->rangeVar->varattno;
+	if (resultRelInfo->ri_RootResultRelInfo)
+		map = ExecGetChildToRootMap(resultRelInfo);
+	if (map != NULL)
+		rangeAttno = map->attrMap->attnums[rangeAttno - 1];
+
+	slot_getallattrs(slot);
+
+	/* Release any value saved from a prior row. */
+	if (fpoState->fp_origNewRangeValid)
+	{
+		fpoState->fp_origNewRangeValid = false;
+		if (!fpoState->fp_origNewRangeIsNull)
+			pfree(DatumGetPointer(fpoState->fp_origNewRange));
+	}
+
+	if (slot->tts_isnull[rangeAttno - 1])
+	{
+		fpoState->fp_origNewRange = (Datum) 0;
+		fpoState->fp_origNewRangeIsNull = true;
+	}
+	else
+	{
+		/*
+		 * Make sure we copy everything for pass-by-reference types
+		 * (like range and multirange).
+		 */
+		oldcontext = MemoryContextSwitchTo(mtstate->ps.state->es_query_cxt);
+		fpoState->fp_origNewRange = datumCopy(slot->tts_values[rangeAttno - 1],
+											  false, -1);
+		MemoryContextSwitchTo(oldcontext);
+		fpoState->fp_origNewRangeIsNull = false;
+	}
+	fpoState->fp_origNewRangeValid = true;
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1810,7 +1936,8 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
 	/* Compute temporal leftovers in FOR PORTION OF */
 	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
-		ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid);
+		ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid,
+								  NULL);
 
 	/* AFTER ROW DELETE Triggers */
 	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
@@ -2390,6 +2517,13 @@ ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		if (context->estate->es_insert_pending_result_relations != NIL)
 			ExecPendingInserts(context->estate);
 
+		/*
+		 * For FOR PORTION OF, remember the range column value so we can
+		 * later detect whether a BEFORE trigger changed it.
+		 */
+		if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+			ExecForPortionOfSaveRange(context, resultRelInfo, slot);
+
 		return ExecBRUpdateTriggers(context->estate, context->epqstate,
 									resultRelInfo, tupleid, oldtuple, slot,
 									result, &context->tmfd,
@@ -2615,7 +2749,8 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
 	/* Compute temporal leftovers in FOR PORTION OF */
 	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
-		ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+		ExecForPortionOfLeftovers(context, context->estate, resultRelInfo,
+								  tupleid, slot);
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(context->estate, resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 13359180d25..efcd52411ab 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -483,6 +483,11 @@ 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 */
+	Datum		fp_origNewRange;	/* range column value captured just before
+									 * BEFORE UPDATE triggers fire, so we can
+									 * detect whether they changed it */
+	bool		fp_origNewRangeIsNull;
+	bool		fp_origNewRangeValid;	/* is fp_origNewRange meaningful? */
 } ForPortionOfState;
 
 /*
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 31f772c723d..f9b9c1d0d6d 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -1793,6 +1793,26 @@ SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
 (3 rows)
 
 DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
+-- A BEFORE UPDATE trigger that changes the application-time column must
+-- raise an error, just as an explicit SET on that column does.
+CREATE FUNCTION trg_fpo_change_valid_at()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  NEW.valid_at = daterange('2018-01-01', '2019-01-01');
+  RETURN NEW;
+END;
+$$;
+CREATE TRIGGER fpo_before_update_row
+  BEFORE UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_change_valid_at();
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '!')
+  WHERE id = '[1,2)';
+ERROR:  cannot change column "valid_at" from a BEFORE trigger because it is used in FOR PORTION OF
+DROP TRIGGER fpo_before_update_row ON for_portion_of_test;
+DROP FUNCTION trg_fpo_change_valid_at();
 -- Test with multiranges
 CREATE TABLE for_portion_of_test2 (
   id int4range NOT NULL,
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index d4062acf1d1..39bb17a9409 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1169,6 +1169,30 @@ SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at;
 
 DROP TRIGGER fpo_after_delete_row ON for_portion_of_test;
 
+-- A BEFORE UPDATE trigger that changes the application-time column must
+-- raise an error, just as an explicit SET on that column does.
+
+CREATE FUNCTION trg_fpo_change_valid_at()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  NEW.valid_at = daterange('2018-01-01', '2019-01-01');
+  RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER fpo_before_update_row
+  BEFORE UPDATE ON for_portion_of_test
+  FOR EACH ROW EXECUTE PROCEDURE trg_fpo_change_valid_at();
+
+UPDATE for_portion_of_test
+  FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01'
+  SET name = CONCAT(name, '!')
+  WHERE id = '[1,2)';
+
+DROP TRIGGER fpo_before_update_row ON for_portion_of_test;
+DROP FUNCTION trg_fpo_change_valid_at();
+
 -- Test with multiranges
 
 CREATE TABLE for_portion_of_test2 (
-- 
2.47.3

---

From: Tom Lane <[email-masked]>
Date: Sun, 19 Apr 2026 14:10:02 -0400

Peter Eisentraut <[email-masked]> writes:
> I have committed the patches 0001 through 0003.

Coverity is complaining that rsi.isDone may be used uninitialized in
ExecForPortionOfLeftovers.  It's correct: that function is not obeying
the function call protocol, and it's only accidental that it's not
failing.  In ValuePerCall mode the caller is supposed to initialize
isDone (and isnull too) before each call.  The canonical reference
for this is execSRF.c, and it does that.  So I think we need something
like the attached.

I notice that execSRF.c also runs pgstat_init_function_usage and
pgstat_end_function_usage around each call.  That's not too important
right now, but I wonder whether we should add it while we're looking
at this.  It would perhaps be important once we support user-defined
withoutPortionProcs.

			regards, tom lane

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index ef2a6bc6e9d..b852b96839d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1514,6 +1514,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	rsi.expectedDesc = NULL;
 	rsi.allowedModes = (int) (SFRM_ValuePerCall);
 	rsi.returnMode = SFRM_ValuePerCall;
+	/* isDone is filled below */
 	rsi.setResult = NULL;
 	rsi.setDesc = NULL;
 
@@ -1537,14 +1538,22 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	 */
 	while (true)
 	{
-		Datum		leftover = FunctionCallInvoke(fcinfo);
+		Datum		leftover;
+
+		/* Call the function one time */
+		fcinfo->isnull = false;
+		rsi.isDone = ExprSingleResult;
+		leftover = FunctionCallInvoke(fcinfo);
+
+		if (rsi.returnMode != SFRM_ValuePerCall)
+			elog(ERROR, "without_portion function violated function call protocol");
 
 		/* Are we done? */
 		if (rsi.isDone == ExprEndResult)
 			break;
 
 		if (fcinfo->isnull)
-			elog(ERROR, "Got a null from without_portion function");
+			elog(ERROR, "got a null from without_portion function");
 
 		/*
 		 * Does the new Datum violate domain checks? Row-level CHECK

---

From: Paul A Jungwirth <[email-masked]>
Date: Sun, 19 Apr 2026 11:51:23 -0700

> for this is execSRF.c, and it does that.  So I think we need something

Thanks for the patch! Your changes look good to me.

> at this.  It would perhaps be important once we support user-defined

I agree we should do that. Here is a patch with that added to your changes.

I was curious why execSRF.c uses `rsinfo.isDone != ExprMultipleResult`
for the finalize parameter, because I don't think a SRF should ever
return ExprSingleResult, right? So I guess it is just to be cautious.
Makes sense. I followed that approach.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From ebb58c1af3eb280abec988d258c349cc9377ae67 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 19 Apr 2026 11:30:17 -0700
Subject: [PATCH v2] Make FOR PORTION OF obey SRF protocol

This fixes a Coverity error about rsi.isDone not being initialized. The built-in
{multi,}range_minus_multi functions don't return without setting it, but a
user-supplied function might not be as accommodating.

We also add statistics tracking around the function call.
---
 src/backend/executor/nodeModifyTable.c | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index ef2a6bc6e9d..353a05cadff 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -65,6 +65,7 @@
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
+#include "pgstat.h"
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "storage/lmgr.h"
@@ -1419,6 +1420,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	CmdType		oldOperation;
 	TransitionCaptureState *oldTcs;
 	FmgrInfo	flinfo;
+	PgStat_FunctionCallUsage fcusage;
 	ReturnSetInfo rsi;
 	bool		didInit = false;
 	bool		shouldFree = false;
@@ -1514,6 +1516,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	rsi.expectedDesc = NULL;
 	rsi.allowedModes = (int) (SFRM_ValuePerCall);
 	rsi.returnMode = SFRM_ValuePerCall;
+	/* isDone is filled below */
 	rsi.setResult = NULL;
 	rsi.setDesc = NULL;
 
@@ -1537,14 +1540,27 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	 */
 	while (true)
 	{
-		Datum		leftover = FunctionCallInvoke(fcinfo);
+		Datum		leftover;
+
+		pgstat_init_function_usage(fcinfo, &fcusage);
+
+		/* Call the function one time */
+		fcinfo->isnull = false;
+		rsi.isDone = ExprSingleResult;
+		leftover = FunctionCallInvoke(fcinfo);
+
+		pgstat_end_function_usage(&fcusage,
+								  rsi.isDone != ExprMultipleResult);
+
+		if (rsi.returnMode != SFRM_ValuePerCall)
+			elog(ERROR, "without_portion function violated function call protocol");
 
 		/* Are we done? */
 
 		if (fcinfo->isnull)
 
 		/*

---

From: Tom Lane <[email-masked]>
Date: Mon, 20 Apr 2026 10:33:54 -0400

> I agree we should do that. Here is a patch with that added to your changes.

Pushed, thanks.

> Makes sense. I followed that approach.

It's been awhile, but I think these specs were set with the intention
that if a plain function were somehow called as a SRF, it would act as
though it were a SRF returning one row.  We haven't quite reached that
with this patch --- I think it'd be an infinite loop as
ExecForPortionOfLeftovers() stands.  I'm content with the way things
are though, given that it should always be the case that special
privileges are needed to mark a function as being a
withoutPortionProcs function.

But speaking of infinite loops, should this one contain a
CHECK_FOR_INTERRUPTS call?  It's hard to conceive of a case where
the value would be broken down finely enough for that to be a
problem, but ...

			regards, tom lane

---

From: Paul A Jungwirth <[email-masked]>
Date: Mon, 20 Apr 2026 08:48:31 -0700

> the value would be broken down finely enough for that to be a

A rangetype could only loop 0-2 times; a multirange 0-1. So I don't
think we need it. Eventually user-defined types could loop more, but a
design that inserts many records every time you change something seems
like a bad idea. Maybe I would add it anyway just out of caution, but
I suspect it's excessive.

Yours,

---

From: Tom Lane <[email-masked]>
Date: Mon, 20 Apr 2026 12:03:30 -0400

> I suspect it's excessive.

Fair enough.  It's quite likely that we'd hit at least one CFI down
inside the insertion anyway.

			regards, tom lane

---

From: jian he <[email-masked]>
Date: Tue, 21 Apr 2026 14:25:06 +0800

hi.

CREATE TABLE fpo_instead_base (id int, valid_at daterange, val int);
INSERT INTO fpo_instead_base VALUES (1, '[2024-01-01,2024-12-31)', 100);
CREATE VIEW fpo_instead_view AS SELECT * FROM fpo_instead_base;
CREATE FUNCTION fpo_instead_trig_fn() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
    RETURN NEW;
END;
$$;
CREATE TRIGGER fpo_instead_trig INSTEAD OF UPDATE ON fpo_instead_view
  FOR EACH ROW EXECUTE FUNCTION fpo_instead_trig_fn();

UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO
'2024-08-01'
    SET val = 999 WHERE id = 1
    RETURNING *;

 id |        valid_at         | val
----+-------------------------+-----
  1 | [2024-01-01,2024-12-31) | 999
(1 row)

Should I expect the column `valid_at` value as [2024-04-01,2024-08-01) ?

We should also document this on doc/src/sgml/ref/update.sgml
Attached is a minor regession test enhancement for
"v2-0001-Fix-INSTEAD-OF-triggers-with-DELETE-UPDATE-FOR-PO.patch".

---

From: jian he <[email-masked]>
Date: Tue, 21 Apr 2026 17:51:18 +0800

> errcodes is correct though.
HI.
After applying v1-0001-Forbid-BEFORE-UPDATE-triggers-changing-the-FOR-PO.patch

----------------------------------------------------
CREATE OR REPLACE FUNCTION trg_fponum() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
  NEW.valid_at = '[1,12)';
  raise notice 'old: %, new: %', old, new;
  RETURN NEW;
END;
$$;
create table fpo3(valid_at int4range, b int);
CREATE TRIGGER fpo_before_update_row BEFORE UPDATE ON fpo3 FOR EACH
ROW EXECUTE PROCEDURE trg_fponum();
insert into fpo3 values('[1,100]', 1);
UPDATE fpo3 FOR PORTION OF valid_at FROM 1 TO 12 SET b = 2;
----------------------------------------------------
The above works as expected, but the below is not what i expected.

create type textrange as range (subtype = text, collation = "C");
CREATE OR REPLACE FUNCTION trg_fpo()
RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
  NEW.valid_at = '[A,d)';
  raise notice 'old: %, new: %', old, new;
  RETURN NEW;
END;
$$;

create table fpo1(valid_at textrange, b int);
CREATE TRIGGER fpo_before_update_row BEFORE UPDATE ON fpo1 FOR EACH
ROW EXECUTE PROCEDURE trg_fpo();
insert into fpo1 values ('[a,d]', 1);

UPDATE fpo1 FOR PORTION OF valid_at FROM 'A' TO 'd' SET b = 2;
NOTICE:  old: ("[a,d]",1), new: ("[A,d)",2)
ERROR:  cannot change column "valid_at" from a BEFORE trigger because
it is used in FOR PORTION OF

Should I expect this to work without error, just like the table fpo3
UPDATE FOR PORTION OF statement above?

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 22 Apr 2026 12:50:27 -0700

> UPDATE fpo_instead_view FOR PORTION OF valid_at FROM '2024-04-01' TO

Yes, because we ran an INSTEAD OF trigger and skipped the UPDATE
(including setting the start/end dates).

> "v2-0001-Fix-INSTEAD-OF-triggers-with-DELETE-UPDATE-FOR-PO.patch".

Thanks! I squashed those patches and did some minor cleanup. I posted
v4 to this dedicated thread:
https://www.postgresql.org/message-id/CA%2BrenyVenLk%2Bu%3DyGvDAyeFEuvkmeQx448-KnnGczqQHB10_fbg%40mail.gmail.com

I also made a commitfest entry pointing there. Let's continue on that
thread so that future messages & patches get tracked correctly (and
not as part of the original feature's CF entry).

Hmm I forgot to add the documentation first. So I'll do that and post
a v5 shortly.

Yours,