Subject: SQL:2011 application time

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 30 Jun 2021 10:39:00 -0700

Hello,

Here is a set of patches to add SQL:2011 application-time support (aka
valid-time).
Previous discussion was on
https://www.postgresql.org/message-id/[email-masked]
but I thought I should update the email subject.

There are four patches here:

- Add PERIODs.
- Add temporal PRIMARY KEY and UNIQUE constraints.
- Add UPDATE/DELETE FOR PORTION OF.
- Add temporal FOREIGN KEYs.

The PERIOD patch is mostly Vik Fearing's work (submitted here a few
years ago), so he should get credit for that!

All patches have tests & documentation. I do have a few more tests I
plan to write, and there are some questions for reviewers embedded in
patches (mostly about when to lock and/or copy data structures). I've
tried to format these as C++ comments to indicate they should be
removed before committing.

Throughout I've made sure that wherever SQL:2011 accepts a PERIOD, we
also accept a range column. So in all these examples valid_at could be
either one:

    PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
    FOREIGN KEY (id, PERIOD valid_at)
    REFERENCES too (id, PERIOD valid_at)
    FOR PORTION OF valid_at FROM t1 TO t2

Range types are superior to PERIODs in many ways, so I think we should
support both. For example you can SELECT them, WHERE them, GROUP BY
them, pass them to functions, return them from functions, do
arithmetic on them, index them, etc.

In fact whether you use a PERIOD or a range, the implementation uses
ranges a lot, since they are such a good fit. A temporal PK is really
an exclusion constraint, etc. When you define a PERIOD, we find a
matching range type and store its oid on the period record. If there
are more than one range type we raise an error, but you can give a
rangetype option to remove the ambiguity. This means we support
PERIODs of any type (basically), not just dates & timestamps.

According to SQL:2011 we should automatically set any columns used by
a PERIOD to NOT NULL. I've ignored that requirement, since permitting
nullable columns is strictly greater functionality: you can always
make the columns NOT NULL if you like. Interpreting NULLs as unbounded
fits better with our range types, and it means you don't have to use
sentinels. (Timestamp has +-Infinity, but many types don't.) Oracle
also accepts null PERIOD columns and treats them the same way. I don't
think it would break anything though to force PERIOD columns to NOT
NULL. If you hate sentinels you can just use range columns. But still
I see no reason to force this on our users.

In the FOR PORTION OF bounds I accept MINVALUE and MAXVALUE as special
tokens. I chose the names to be consistent with partition syntax. This
isn't part of the standard but seems nice.

Here are a few other things to discuss:

- My patch only adds application time. There is a separate patch to
add system time: https://commitfest.postgresql.org/33/2316/ I don't
foresee any serious conflicts between our work, and in general I think
each patch implements its functionality at an appropriate (but
different) level of abstraction. But I haven't looked at that patch
recently. I'll try to give some comments during this commitfest. The
one place they probably overlap is with defining PERIODs. Since
system-time periods *must* be named SYSTEM_TIME, even that overlap
should be slight, but it still might be worth accepting the PERIOD
patch here before adopting either. Even SYSTEM_TIME ought to be
recorded in information_schema.periods IIRC.

- The biggest thing remaining to do is to add support for partitioned
tables. I would love some help with that if anyone is interested.

- Since temporal PKs are implemented with exclusion constraints they
use GiST indexes, so you can't really use them without the btree_gist
extension (unless *all* your key parts are ranges---which is how we
test exclusion constraints). Personally I'm okay with this, since even
exclusion constraints are pretty useless without that extension. But
it seems like something to talk about.

- At PgCon 2020 Vik suggested a different way of querying for FK
checks, which he used in his own temporal tables extension. It is more
complicated but he thinks it may be faster. I plan to try both and run
some benchmarks. I'm not sure whether his approach will work with
CASCADE/SET NULL/SET DEFAULT---but I haven't looked at it in a while.

- It is hard to avoid a shift/reduce conflict in FOR PORTION OF
<period_or_range> FROM <expr> TO <expr> because expressions may
contain date INTERVALs that also may contain TO. So this is an error:

    FOR PORTION OF valid_at
      FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
      TO   '2019-01-01'

but this works:

   FOR PORTION OF valid_at
     FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
     TO   '2019-01-01'

I'm personally satisfied with that, but if anyone thinks it can be
improved please let me know. It would be nice if the parser were smart
enough to see that without a second TO, it must belong to FOR PORTION
OF, not the interval. But *I'm* not smart enough to teach it that. :-)
If only it could have a greater lookahead. . . .

- Normally we return the number of rows affected by an UPDATE/DELETE.
What do you think we should do when a FOR PORTION OF causes extra rows
to be inserted? I'm not doing anything special here today. After all
foreign keys don't do anything extra when they CASCADE/SET (to my
knowledge). Also I think adding info about the inserted rows might be
annoying, since I'd have to communicate it from within the trigger
function. I'm really hoping no one asks for this.

- Since PERIODs are a weird neither-fish-nor-foul thing (parsed a lot
like a column, but also behaving like a constraint), they add a lot of
tedious if-statements when they are used by an index or constraint. In
many places I've used a zero attnum to signal that a component is
really a PERIOD. (Range columns are easy since they really are a
column.) I feel this approach is pretty ugly, so I will probably
experiment a bit with a different way. If anyone else wants to take
this on though, I'm grateful for the help.

- It would be really cool if ON CONFLICT DO UPDATE had a temporal
variant so it would INSERT the missing durations and UPDATE the
existing ones. That's what Tom Johnston said the standard should have
required in *Bitemporal Data*, and it does make things a lot easier on
the client side. But that is something to do in a later patch. . . .

Yours,
Paul

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 2 Jul 2021 14:39:50 -0700

> Here is a set of patches to add SQL:2011 application-time support (aka

Here is a small fix to prevent `FOR PORTION OF valid_at FROM MAXVALUE
TO foo` or `FROM foo TO MINVALUE`. I rebased on latest master too.

Yours,
Paul

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 3 Jul 2021 10:46:55 -0700

> TO foo` or `FROM foo TO MINVALUE`. I rebased on latest master too.

Here is a patch set that cleans up the catalog docs for pg_period. The
columns have changed since that was written, and also we use a
different sgml structure on those pages now. Note pg_period still
contains a couple essentially-unused columns, perislocal and
perinhcount. Those are intended for supporting table inheritance, so
I've left them in.

Paul

---

From: Jaime Casanova <[email-masked]>
Date: Sat, 4 Sep 2021 14:55:56 -0500

> perinhcount. Those are intended for supporting table inheritance, so

Hi Paul,

Thanks for working on this. It would be a great improvement.

I wanted to test the patches but:

patch 01: does apply but doesn't compile, attached the compile errors.
patch 04: does not apply clean.

Please fix and resend.

-- 
Jaime Casanova
Director de Servicios Profesionales
SystemGuards - Consultores de PostgreSQL

---

From: Paul A Jungwirth <[email-masked]>
Date: Mon, 6 Sep 2021 12:52:37 -0700

> patch 04: does not apply clean.

Thanks for taking a look! I've rebased & made it compile again. v7 attached.

Yours,
Paul

---

From: Zhihong Yu <[email-masked]>
Date: Mon, 6 Sep 2021 13:40:13 -0700

> Thanks for taking a look! I've rebased & made it compile again. v7
Hi,
For v7-0001-Add-PERIODs.patch :

+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group

It seems the year (2018) should be updated to 2021.

For RemovePeriodById(), it seems table_open() can be called
after SearchSysCache1(). This way, if HeapTupleIsValid(tup) is true,
table_open() can be skipped.

For tablecmds.c, AT_PASS_ADD_PERIOD is defined as 5 with AT_PASS_ADD_CONSTR
etc moved upward. Do we need to consider compatibility ?

There are a few TODO's such as:
+    * TODO: What about periods?

Are they going to be addressed in the next round of patches ?

There seems to be some overlap between ATExecAddPeriod()
and AddRelationNewPeriod().
Is it possible to reduce code duplication ?

Cheers

---

From: Jaime Casanova <[email-masked]>
Date: Fri, 10 Sep 2021 20:50:17 -0500

> Thanks for taking a look! I've rebased & made it compile again. v7 attached.

patch 01: does apply but gives a compile warning (which is fixed by patch
02)
"""
parse_utilcmd.c: In function ‘generateClonedIndexStmt’:
parse_utilcmd.c:1730:2: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
  Period *p = makeNode(Period);
  ^~~~~~
"""

patch 03: produces these compile errors.  

analyze.c: In function ‘transformForPortionOfBound’:
analyze.c:1171:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
   A_Const    *n2 = makeNode(A_Const);
   ^~~~~~~
analyze.c:1172:10: error: ‘union ValUnion’ has no member named ‘type’
   n2->val.type = T_Null;
          ^
analyze.c:1172:18: error: ‘T_Null’ undeclared (first use in this function)
   n2->val.type = T_Null;
                  ^~~~~~
analyze.c:1172:18: note: each undeclared identifier is reported only once for each function it appears in

---

From: Paul A Jungwirth <[email-masked]>
Date: Sun, 12 Sep 2021 21:12:19 -0700

On Fri, Sep 10, 2021 at 6:50 PM Jaime Casanova
<[email-masked]> wrote:
> [snip]

I did a rebase and fixed this new error, as well as the warnings.

> It seems the year (2018) should be updated to 2021.

Done.

> For RemovePeriodById(), it seems table_open() can be called after SearchSysCache1(). This way, if HeapTupleIsValid(tup) is true, table_open() can be skipped.

This seems like it permits a race condition when two connections both
try to drop the period, right?

> For tablecmds.c, AT_PASS_ADD_PERIOD is defined as 5 with AT_PASS_ADD_CONSTR etc moved upward. Do we need to consider compatibility ?

I don't think there is a compatibility problem---can you explain?
These symbols aren't used outside tablecmds.c and the values aren't
saved anywhere AFAIK.

> Are they going to be addressed in the next round of patches ?

These are mostly questions I'm hoping a reviewer can help me answer,
but I'll take a pass through them and see which I can remove myself.
Several are for adding support for partitioned tables, where I would
definitely appreciate help.

> There seems to be some overlap between ATExecAddPeriod() and AddRelationNewPeriod().

I've refactored those functions to remove some duplication, but I
think I prefer the old version---let me know if you have suggestions
to avoid the duplication in a nicer way.

Oh also I realized fp_triggers.c wasn't included in the last few patch
files---I'm sorry about that!

Latest files attached. Thanks for the reviews!

Paul

---

From: Corey Huinker <[email-masked]>
Date: Mon, 13 Sep 2021 02:05:41 -0400

So I've been eagerly watching this thread and hoping to have time to devote
to it. I've also been looking at the thread at
https://www.postgresql.org/message-id/[email-masked]
that covers system versioning, and per our conversation far too long ago
(again, my bad) it's obvious that the two efforts shouldn't do anything
that would be in conflict with one another, as we eventually have to
support bitemporal [1] tables: tables that have both system versioning and
an application period.

Below is a list of observations and questions about this proposed patch of
itself in isolation, but mostly about how it relates to the work being done
for system versioning.

1. This patch creates a pg_period catalog table, whereas the system
versioning relies on additions to pg_attribute to identify the start/end
columns. Initially I thought this was because it was somehow possible to
have *multiple* application periods defined on a table, but in reading [1]
I see that there are some design suppositions that would make a second
application period impossible[2]. I can also see where having this table
would facilitate the easy creation of INFORMATION_SCHEMA.PERIODS. I was
previously unaware that this info schema table was a thing, but I have
found references to it, though I'm unclear as to whether it's supposed to
have information about system versioned tables in it as well.

Q 1.1. Would a bitemporal table have two entries in that view?
Q 1.2. Could you see being able to implement this without pg_period, using
only additions to pg_attribute (start/end for system temporal, start/end
for application, plus an addition for period name)?
Q 1.3. Can you see a way to represent the system versioning in pg_period
such that bitemporal tables were possible?

 2. The system versioning effort has chosen 'infinity' as their end-time
value, whereas you have chosen NULL as that makes sense for an unbounded
range. Other databases seem to leverage '9999-12-31 23:59:59' (SQLServer,
IIRC) whereas some others seem to used '2999-12-31 23:59:59' but those
might have been home-rolled temporal implementations. To further add to the
confusion, the syntax seems to specify the keyword of MAXVALUE, which
further muddies things. The system versioning people went with 'infinity'
seemingly because it prescribe and end to the world like SQLServer did, but
also because it allowed for a primary key based on (id, endtime) and that's
just not possible with NULL endtime values.

Q 2.1. Do you have any thoughts about how to resolve this notational logjam?

3. I noticed some inconsistency in the results from various "SELECT * FROM
portion_of_test" examples. In some, the "valid_at" range is shown but not
columns that make it up, and in some others, the "valid_from" and
"valid_to" columns are shown, with no mention of the period. From what I've
seen, the period column should be invisible unless invoked, like ctid or
xmin.

4. The syntax '2018-03-04' AT TIME ZONE INTERVAL '2'  HOUR TO MINUTE simply
confounded me. I googled around for it, but could find no matches for
postgres exception in mailing list discussions circa 2003. I tried it out
myself and, lo and behold

# SELECT '2018-03-04' AT TIME ZONE INTERVAL '2'  HOUR TO MINUTE;
      timezone
---------------------
 2018-03-04 05:02:00
(1 row)

I really didn't expect that to work, or even "work". I can see that it
added 2 minutes to UTC's perspective on my local concept of midnight, but I
don't understand what it's supposed to mean.

Q 4.1. What does it mean?

5. I haven't seen any actual syntax conflicts between this patch and the
system versioning patch. Both teams added basically the same keywords,
though I haven't dove more deeply into any bison incompatibilities. Still,
it's a great start.

6. Overall, I'm really excited about what this will mean for data
governance in postgres.

[1]
https://cs.ulb.ac.be/public/_media/teaching/infoh415/tempfeaturessql2011.pdf
[2] In the bitemporal table example in [1] - the application period get the
defined primary key, and the system_time period would be merely unique

On Mon, Sep 13, 2021 at 12:12 AM Paul A Jungwirth <
[email-masked]> wrote:

> > For RemovePeriodById(), it seems table_open() can be called after
> SearchSysCache1(). This way, if HeapTupleIsValid(tup) is true, table_open()
> can be skipped.
>
> > For tablecmds.c, AT_PASS_ADD_PERIOD is defined as 5 with
> AT_PASS_ADD_CONSTR etc moved upward. Do we need to consider compatibility ?
>

---

From: Paul A Jungwirth <[email-masked]>
Date: Mon, 13 Sep 2021 20:56:47 -0700

Hi Corey,

Thanks for all the good questions!

> 1. This patch creates a pg_period catalog table, whereas the system versioning relies on additions to pg_attribute to identify the start/end columns. Initially I thought this was because it was somehow possible to have multiple application periods defined on a table, but in reading [1] I see that there are some design suppositions that would make a second application period impossible[2]. I can also see where having this table would facilitate the easy creation of INFORMATION_SCHEMA.PERIODS. I was previously unaware that this info schema table was a thing, but I have found references to it, though I'm unclear as to whether it's supposed to have information about system versioned tables in it as well.

Yes, information_schema.periods is given by the standard. Having
pg_period seems like a natural place to store periods, since they are
separate entities. I think that is a better design than just storing
them as extra fields in pg_attribute. It follows normal normalization
rules.

The standard forbids multiple application-time periods per table. From
SQL:2011 in the SQL/Foundation section
(7IWD2-02-Foundation-2011-12.pdf available from
http://www.wiscorp.com/sql20nn.zip) under 11.27 <add table period
definition>:

> 5) If <table period definition> contains <application time period specification> ATPS, then:
>    b) The table descriptor of T shall not include a period descriptor other than a system-time period descriptor.

In other words you can add both a SYSTEM TIME period and one other
application-time period (whose name is your choice), but if you
already have an application-time period, you can't add another one.

I also checked other RDBMSes and none of them allow it either:

In Mariadb 10.6.4 (the latest) I get "ERROR 4154 (HY000); Cannot
specify more than one application-time period".

Oracle disallows it with a vague error:

      SQL> create table t2 (id int, valid_from date, valid_til date,
period for valid_at (valid_from, valid_til), period for valid_at2
valid_from, valid_til));
      create table t2 (id int, valid_from date, valid_til date, period
for valid_at (valid_from, valid_til), period for valid_at2
(valid_from, valid_til))

                         *
      ERROR at line 1:
      ORA-55603: invalid flashback archive or valid time period command

(Using different start/end columns for each period doesn't change the result.)

In IBM DB2 you can only have one because application-time periods must
be named "business_time" (not joking).

Mssql (2019) doesn't support application periods.

Personally I feel like it's a weird limitation and I wouldn't mind
supporting more, but my current implementation only allows for one,
and I'd have to rethink some things to do it differently.

Also: I think information_schema.periods *should* include SYSTEM_TIME
periods. The spec says (in SQL/Schemata, file
7IWD2-11-Schemata-2011-12.pdf at the link above), "The PERIODS base
table has one row for each period defined for a table. It effectively
contains a representation of the period descriptors." It doesn't say
anything about excluding system-time periods.

I checked mariadb, mssql, oracle, and db2, and I could only find this
table in db2, as syscat.periods. It includes both application-time and
system-time periods.

The spec calls for the columns table_catalog, table_schema,
table_name, period_name, start_column_name, and end_column_name. There
isn't a column to distinguish the period type, but since a period is a
system-time period iff its name is "SYSTEM_TIME", technically such a
column isn't needed.

The db2 columns are periodname, tabschema, tabname, begincolname,
endcolname, periodtype, historytabschema, and historytabname. The
periodtype column is either A or S (for application-time or
system-time).

> Q 1.1. Would a bitemporal table have two entries in that view?

Yes.

> Q 1.2. Could you see being able to implement this without pg_period, using only additions to pg_attribute (start/end for system temporal, start/end for application, plus an addition for period name)?

Not just period name, but also the range type associated with the
period (which should be determined at period creation, so that you can
pass an option to disambiguate if there are two ranges defined for the
same base type), the constraint oid (to prevent end <= start), and
some more data for inherited tables (not really used yet). It seems
ugly to hang all these extra values on a pg_attribute record.

> Q 1.3. Can you see a way to represent the system versioning in pg_period such that bitemporal tables were possible?

Yes. Even though the name "SYSTEM_TIME" is technically enough, I'd
still include a pertype column to make distinguishing system vs
application periods easier and more obvious.

> 2. The system versioning effort has chosen 'infinity' as their end-time value, whereas you have chosen NULL as that makes sense for an unbounded range. Other databases seem to leverage '9999-12-31 23:59:59' (SQLServer, IIRC) whereas some others seem to used '2999-12-31 23:59:59' but those might have been home-rolled temporal implementations. To further add to the confusion, the syntax seems to specify the keyword of MAXVALUE, which further muddies things. The system versioning people went with 'infinity' seemingly because it prescribe and end to the world like SQLServer did, but also because it allowed for a primary key based on (id, endtime) and that's just not possible with NULL endtime values.

I think it's a little weird that our system-time patch mutates your
primary key. None of the other RDMBSes do that. I don't think it's
incompatible (as long as the system time patch knows how to preserve
the extra period/range data in an application-time temporal key), but
it feels messy to me.

I would prefer if system-time and application-time used the same value
to mean "unbounded". Using null means we can support any type (not
just types with +-Infinity). And it pairs nicely with range types. If
the only reason for system-time to use Infinity is the primary key, I
think it would be better not to mutate the primary key (and store the
historical records in a separate table as other RDMSes do).

Btw Oracle also uses NULL to mean "unbounded".

We presently forbid PKs from including expressions, but my patch lifts
that exception so it can index a rangetype expression built from the
period start & end columns. So even if we must include the system-time
end column in a PK, perhaps it can use a COALESCE expression to store
Infinity even while using NULL to signify "currently true" from a user
perspective.

> 3. I noticed some inconsistency in the results from various "SELECT * FROM portion_of_test" examples. In some, the "valid_at" range is shown but not columns that make it up, and in some others, the "valid_from" and "valid_to" columns are shown, with no mention of the period. From what I've seen, the period column should be invisible unless invoked, like ctid or xmin.

In most cases the tests test the same functionality with both PERIODs
and rangetype columns. For FKs they test all four combinations of
PERIOD-referencing-PERIOD, PERIOD-referencing-range,
range-referencing-PERIOD, and range-referencing-range. If valid_at is
a genuine column, it is included in SELECT *, but not if it is a
PERIOD.

> 4. The syntax '2018-03-04' AT TIME ZONE INTERVAL '2'  HOUR TO MINUTE simply confounded me.

Me too! I have no idea what that is supposed to mean. But that
behavior predates my patch. I only had to deal with it because it
creates a shift-reduce conflict with `FOR PORTION OF valid_at FROM x
TO y`, where x & y are expressions. I asked about this syntax at my
PgCon 2020 talk, but I haven't ever received an answer. Perhaps
someone else knows what this kind of INTERVAL means (as a modifier of
a time value).

> 5. I haven't seen any actual syntax conflicts between this patch and the system versioning patch. Both teams added basically the same keywords, though I haven't dove more deeply into any bison incompatibilities. Still, it's a great start.

I think that's right. Early on the other patch used `FOR PERIOD SYSTEM
TIME (x, y)` instead of the standard `FOR PERIOD SYSTEM_TIME (x, y)`
but I believe that was fixed, so that the period name is an identifier
and not two keywords.

> 6. Overall, I'm really excited about what this will mean for data governance in postgres.

Me too, and thank you for the detailed review!

Yours,
Paul

---

From: Corey Huinker <[email-masked]>
Date: Sat, 18 Sep 2021 20:46:16 -0400

> be named "business_time" (not joking).

I saw that as well, and it made me think that someone at IBM is a fan of
Flight Of The Conchords.

> and I'd have to rethink some things to do it differently.

I'm satisfied that it's not something we need to do in the first MVP.

> application periods easier and more obvious.

SYSTEM_TIME seems to allow for DATE values in the start_time and end_time
fields, though I cannot imagine how that would ever be practical, unless it
were somehow desirable to reject subsequent updates within a 24 hour
timeframe. I have seen instances where home-rolled application periods used
date values, which had similar problems where certain intermediate updates
would simply have to be discarded in favor of the one that was still
standing at midnight.

> the extra period/range data in an application-time temporal key), but

Per outline below, I'm proposing an alternate SYSTEM_TIME implementation
that would leave the PK as-is.

> historical records in a separate table as other RDMSes do).

The two  "big wins" of infinity seemed (to me) to be:

1. the ability to add "AND end_time = 'infinity'" as a cheap way to get
current rows
2. clauses like "WHERE CURRENT_DATE - 3 BETWEEN start_time AND end_time"
would work. Granted, there's very specific new syntax to do that properly,
but you know somebody's gonna see the columns and try to do it that way.

> Btw Oracle also uses NULL to mean "unbounded".

Huh, I missed that one. That is good in that it gives some precedence to
how you've approached it.

> Infinity even while using NULL to signify "currently true" from a user

Either way seems viable, but I understand why you want to leverage ranges
in this way.

>
> > 3. I noticed some inconsistency in the results from various "SELECT *
> FROM portion_of_test" examples. In some, the "valid_at" range is shown but
> not columns that make it up, and in some others, the "valid_from" and

Ok, I'll have to look back over the test coverage to make sure that I
understand the behavior now.

> someone else knows what this kind of INTERVAL means (as a modifier of

I think I'll open this as a separate thread, because it would simplify
matters if we can reject this nonsense syntax.

This was the alternative method of system versioning I proposed recently in
the system versioning thread

1. The regular table remains unchanged, but a pg_class attribute named
"relissystemversioned" would be set to true

2. I'm unsure if the standard allows dropping a column from a table while
it is system versioned, and the purpose behind system versioning makes me
believe the answer is a strong "no" and requiring DROP COLUMN to fail
on relissystemversioned = 't' seems pretty straightforward.
3. The history table would be given a default name of $FOO_history (space
permitting), but could be overridden with the history_table option.
4. The history table would have relkind = 'h'
5. The history table will only have rows that are not current, so it is
created empty.
6. As such, the table is effectively append-only, in a way that vacuum can
actually leverage, and likewise the fill factor of such a table should
never be less than 100.
7. The history table could only be updated only via system defined triggers
(insert,update,delete, alter to add columns), or row migration similar to
that found in partitioning. It seems like this would work as the two tables
working as partitions of the same table, but presently we can't have
multi-parent partitions.
8. The history table would be indexed the same as the base table, except
that all unique indexes would be made non-unique, and an index of pk +
start_time + end_time would be added
9. The primary key of the base table would remain the existing pk vals, and
would basically function normally, with triggers to carry forth changes to
the history table. The net effect of this is that the end_time value of all
rows in the main table would always be the chosen "current" value
(infinity, null, 9999-12-31, etc) and as such might not actually _need_ to
be stored.
10. Queries that omit the FOR SYSTEM_TIME clause, as well as ones that use
FOR SYSTEM_TIME AS OF CURRENT_TIMESTAMP, would simply use the base table
directly with no quals to add.
11. Queries that use FOR SYSTEM_TIME and not FOR SYSTEM_TIME AS
OF CURRENT_TIMESTAMP, then the query would do a union of the base table and
the history table with quals applied to both.
12. It's a fair question whether the history table would be something that
could be queried directly. I'm inclined to say no, because that allows for
things like SELECT FOR UPDATE, which of course we'd have to reject.
13. If a history table is directly referenceable, then SELECT permission
can be granted or revoked as normal, but all insert/update/delete/truncate
options would raise an error.
14. DROP SYSTEM VERSIONING from a table would be quite straightforward -
the history table would be dropped along with the triggers that reference
it, setting relissystemversioned = 'f' on the base table.

The benefits to your effort here would be:

1. No change to the primary key except for the ones dictated by application
period
2. The INFORMATION_SCHEMA view need merely take into account The
pg_class.relkind = 'h' entries
3. system versioning is no longer mutating (trigger on X updates X), which
eliminates the possibility that application period triggers get into a loop
4. DROP SYSTEM VERSIONING would be entirely transparent to application
versioning.

Thoughts?

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 1 Oct 2021 13:47:57 -0700

On Sat, Sep 18, 2021 at 5:46 PM Corey Huinker <[email-masked]>
wrote:

> fields,
> though I cannot imagine how that would ever be practical, unless it were
> somehow
> desirable to reject subsequent updates within a 24 hour timeframe.
>

I agree that for SYSTEM_TIME it doesn't make much sense to use anything but
the smallest time granularity.

The two  "big wins" of infinity seemed (to me) to be:
> would work.

Yes. OTOH there is equivalent syntax for ranges, e.g. `valid_at @> now()`.
But if you had a real PERIOD then that wouldn't be available, since you
can't use a PERIOD as an expression. Personally I think that's a shame, and
I wonder if PERIODs should be another kind of expression (much like a
column value) that evaluates to an equivalent range. Then you'd get all
kinds of operators & functions that work with them, you could `SELECT`
them, `GROUP BY` them, pass them to functions, etc.

The spec doesn't say anything about using PERIODs in those places, but it
*does* have a section on period *predicates*, which seem to be allowed
anywhere you can put an expression. The spec's discussion of this is in
4.14.2 ("Operations involving periods") and 8.20 ("<period predicate>"),
and says there should be predicates for overlap, equals, contains,
precedes, succeeds, immediately precedes, and immediately succeeds. So in
the spec, the smallest possible "element" is not a bare PERIOD, but rather
these predicates. My patch doesn't include these (it's a lot of new
syntax), and no other RDBMS seems to have implemented them. I'm inclined to
just treat PERIODs like ranges, or at least maybe let you cast from one to
another. (Casting is weird though since if a bare PERIOD isn't a valid
expression, what are you casting from/to?)

I should add that using +-Infinity for application-time bounds is
completely acceptable under my patch; you just have the option to use NULL
instead. So your examples of filtering above are fine. There aren't any
operations where we have to set a bounded rangepart to unbounded, so we
never pass a NULL; only the user would do that. We do bless NULLs by
translating MINVALUE/MAXVALUE to NULL, but that is necessary to support
arbitrary types. Even that could be refined so that we use +-Infinity when
available but NULL elsewhere. Or we could just drop MINVALUE/MAXVALUE
entirely. It's my own addition to make sentinels less arbitrary; it's not
in the standard.

One of my design goals was to let people favor ranges over PERIODs if they
like. Forcing people to use +-Infinity doesn't completely eliminate that
goal, but it does mean your ranges are different than you're used to seeing
(`[2020-01-01, Infinity)' vs [2020-01-01,)`. More importantly you can only
use {date,ts,tstz}range for application-time periods, not other rangetypes.
So I'd prefer to keep NULL bounds *possible*, even if MINVALUE/MAXVALUE
aren't giving it a sanction.

>     2. I'm unsure if the standard allows dropping a column from a table
> while it is system versioned, and the purpose behind system versioning
> makes me believe the answer is a strong "no" and requiring DROP COLUMN to
> fail on relissystemversioned = 't' seems pretty straightforward.
>     3. The history table would be given a default name of $FOO_history
> (space permitting), but could be overridden with the history_table option.

+1 so far. Behavior of DDL in temporal tables is almost untouched even in
the academic literature I've read. (My bibliography mentions a few places
that at least mention that it's a hard problem.) Forbidding to drop a
column seems pretty harsh---but on the other hand that's just the tip of
the iceberg, so failing is probably the practical choice. For example what
happens to old rows if you add a NOT NULL constraint? For application-time
we can make the user responsible for figuring out the most sensible thing,
but for SYSTEM_TIME we have to figure that out ourselves. But what about
column type changes, or domains? What about removing an enum option? Or
adding a CHECK constraint? With SYSTEM_TIME the user is supposed to be
unable to change the history data, so they can't accommodate it to future
requirements.

> created empty.
>     6. As such, the table is effectively append-only, in a way that vacuum
> can actually leverage, and likewise the fill factor of such a table should
> never be less than 100.
>     7. The history table could only be updated only via system defined
> triggers (insert,update,delete, alter to add columns), or row migration
> similar to that found in partitioning. It seems like this would work as the
> two tables working as partitions of the same table, but presently we can't
> have multi-parent partitions.
>

I don't think they should be sibling partitions, but I do think it would be
cool if you could ask for the history table to be partitioned. Mariadb
offers a way to do this (see my blog post comparing SQL:2011
implementations). It doesn't have to be in the first patch though, and it's
not part of the standard.

    8. The history table would be indexed the same as the base table,
> except that all unique indexes would be made non-unique, and an index of pk
> + start_time + end_time would be added
>

Is there any value to indexing both start_time and end_time? Just one
already takes you to a single row.

The system-time code would need to know how to handle application-time PKs
since they are a little different, but that's not hard. And it still is
just adding a column (or two if you think they should both be there).

The history table also should not have any FKs, and no FKs should reference
it.

    9. The primary key of the base table would remain the existing pk vals,
> and would basically function normally, with triggers to carry forth changes
> to the history table. The net effect of this is that the end_time value of
> all rows in the main table would always be the chosen "current" value

Interesting thought that we wouldn't really even need to store the end
time. I don't have an opinion about whether the optimization is worth the
complexity, but yeah it seems possible.

    10. Queries that omit the FOR SYSTEM_TIME clause, as well as ones that
> use FOR SYSTEM_TIME AS OF CURRENT_TIMESTAMP, would simply use the base
> table directly with no quals to add.
>     11. Queries that use FOR SYSTEM_TIME and not FOR SYSTEM_TIME AS OF
> CURRENT_TIMESTAMP, then the query would do a union of the base table and

I like this, but it means people can't filter directly on the columns
themselves as you suggest above. Can we detect when they're doing that?
Keep in mind it might be happening inside a user-defined function, etc. So
perhaps it is safer to always use the UNION.

    12. It's a fair question whether the history table would be something
> that could be queried directly. I'm inclined to say no, because that allows
> for things like SELECT FOR UPDATE, which of course we'd have to reject.
>     13. If a history table is directly referenceable, then SELECT
> permission can be granted or revoked as normal, but all
> insert/update/delete/truncate options would raise an error.
>

It seems to break the abstraction to let people query the history table
directly. OTOH sometimes it's helpful to see behind the curtain. I could go
either way here, but I slightly favor letting people do it.

    14. DROP SYSTEM VERSIONING from a table would be quite straightforward
> - the history table would be dropped along with the triggers that reference

I like this approach a lot, and I think it's a better design than carrying
all the history inside the main table. I also like how bitemporal will Just
Work^TM. One is in user-space and the other is controlled by
Postgres---which fits the intention.

Yours,
Paul

---

From: Paul Jungwirth <[email-masked]>
Date: Tue, 16 Nov 2021 15:55:26 -0800

Here are some new patches rebased on the latest master.

I haven't made any substantive changes, but I should have time soon to 
take a stab at supporting partitioned tables and removing some of my own 
TODOs (things like making sure I'm locking things correctly). I don't 
think there is any outstanding feedback other than that.

But in the meantime here are some up-to-date patches.

Yours,

-- 
Paul              ~{:-)
[email-masked]

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 20 Nov 2021 17:51:16 -0800

On Tue, Nov 16, 2021 at 3:55 PM Paul Jungwirth <[email-masked]>
wrote:

> TODOs (things like making sure I'm locking things correctly).
>

Hello,

Here are updated patches. They are rebased and clean up some of my TODOs.
Here is what remains:

- Various TODOs asking for advice about concurrency things: where to lock,
when to copy structs, etc. I'd appreciate some review on these from someone
more experienced than me.

- Supporting FOR PORTION OF against updateable views. I'll keep working on
this, but I thought there was enough progress to pass along new patches in
the meantime.

- Support partitioned tables. I think this is a medium-size effort, and I'm
not sure whether it's really needed for pg 15 or something we can add
later. I'm going to do my best to get it done though. (I should have more
time for this project now: having a sixth baby recently made side projects
challenging for a while, but lately things have been getting easier.)
Partitioning could use some design discussion though, both for application
time alone and for bitemporal tables (so overlapping with the system time
work). Here are some thoughts so far:

  - Creating a PERIOD on a partitioned table should automatically create
the PERIOD (and associated constraints) on the child tables. This one seems
easy and I'll try to get it done soon.

  - Sort of related, but not strictly partitioning: CREATE TABLE LIKE
should have a new INCLUDING PERIODS option. (I'm tempted to include this
under INCLUDING CONSTRAINTS, but I think a separate option is nicer since
it gives more control.)

  - If you partition by something in the scalar part of the temporal PK,
that's easy. I don't think we have to do anything special there. I'd like
to add some tests about it though.

  - We should allow temporal primary keys on the top-level partitioned
table, even though they are essentially exclusion constraints. Whereas in
the general case an exclusion constraint cannot prove its validity across
all the tables, a temporal PK *can* prove its validity so long the
partition key includes at least one scalar part of the temporal PK (so that
all records for one "entity" get routed to the same table).

  - If you partition by the temporal part of the temporal PK, things are
harder. I'm inclined to forbid this, at least for v15. Suppose you
partition by the start time. Then you wind up with the same entity spread
across several tables, so you can't validate the overall exclusion
constraint anymore.

  - OTOH you *could* partition by application-time itself (not start time
alone nor end time alone) where each partition has application-time
ranges/periods that are trimmed to fit within that partition's limits. Then
since each partition is responsible for a non-overlapping time period, you
could validate the overall exclusion constraint. You'd just have to add
some logic to tuple re-routing that could transform single records into
multiple records. For example if each partition holds a different year and
you INSERT a record that is valid for a decade, you'd have to insert one
row into ten partitions, and change the application-time range/period of
each row appropriately. This is a special kind of range partitioning. I
don't have any ideas how to make hash or list partitioning work on the
temporal part of the PK. I don't think we should allow it.

  - Partitioning by application time requires no special syntax.
Partitioning by system time (if that's desired) would probably require
extra (non-standard) syntax. Mariadb has this:
https://mariadb.com/kb/en/system-versioned-tables/#storing-the-history-separately
Perhaps that is orthogonal to application-time partitioning though. It
sounds like people think we should store non-current system time in a
separate table (I agree), and in that case I think a bitemporal table that
is partitioned by scalar keys or application-time would just have a
separate system-time history table for each partition, and that would Just
Work. And if we *do* want to partition by system time too, then it would be
transparent to the application-time logic.

  - Since system time doesn't add anything to your PK (or at least it
shouldn't), there is no extra complexity around dealing with exclusion
constraints. We should just guarantee that all *current* rows land in the
same partition, because for a bitemporal table that's the only one that
needs a temporal PK. I guess that means you could partition by end
system-time but not start system-time. This would be an exception to the
rule that a PK must include the partition keys. Instead we'd say that all
current (i.e. non-historical) records stay together (at the system-time
level of partitioning).

  - I don't think system-time partitioning needs to be in v15. It seems
more complicated than ordinary partitioning.

Yours,
Paul

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 17 Jan 2024 19:59:06 -0800

Hello,

Here are new patches consolidating feedback from several emails.
I haven't addressed everything but I think I'm overdue for a reply:

On 1/4/24 21:06, jian he wrote:
 >
 > I am confused.
 > say condition: " (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0)"
 > the following code will only run PartA, never run PartB?
 >
 > `
 > else if (cmp_l1l2 >= 0 && cmp_u1u2 <= 0)
 >      PartA
 > else if (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0)
 >      PartB
 > `
 >
 > minimum example:
 > #include<stdio.h>
 > #include<string.h>
 > #include<stdlib.h>
 > #include<assert.h>
 > int
 > main(void)
 > {
 >      int cmp_l1l2;
 >      int cmp_u1u2;
 >      int cmp_u1l2;
 >      int cmp_l1u2;
 >      cmp_l1u2 = -1;
 >      cmp_l1l2 = 0;
 >      cmp_u1u2 = 0;
 >      cmp_u1l2 = 0;
 >      assert(cmp_u1l2 == 0);
 > if (cmp_l1u2 > 0 || cmp_u1l2 < 0)
 >          printf("calling partA\n");
 >      else if (cmp_l1l2 >= 0 && cmp_u1u2 <= 0)
 >          printf("calling partB\n");
 >      else if (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0)
 >          printf("calling partC\n");
 > }

All of the branches are used. I've attached a `without_portion.c` minimal example showing different 
cases. For ranges it helps to go through the Allen relationships 
(https://en.wikipedia.org/wiki/Allen%27s_interval_algebra) to make a comprehensive check. (But note 
that our operators don't exactly match that terminology, and it's important to consider 
closed-vs-open and unbounded cases.)

 > I am confused with the name "range_without_portion", I think
 > "range_not_overlap" would be better.

I think I covered this in my other reply and we are now in agreement, but if that's mistaken let 
know me.

 > select numrange(1.1, 2.2) @- numrange(2.0, 3.0);
 > the result is not the same as
 > select numrange(2.0, 3.0) @- numrange(1.1, 2.2);

Correct, @- is not commutative.

 > So your categorize oprkind as 'b' for operator "@-" is wrong?
 > select oprname,oprkind,oprcanhash,oprcanmerge,oprleft,oprright,oprresult,oprcode
 > from pg_operator
 > where oprname = '@-';

'b' is the correct oprkind. It is a binary (infix) operator.

 > aslo
 > select count(*), oprkind from pg_operator group by oprkind;
 > there are only 5% are prefix operators.
 > maybe we should design it as:
 > 1. if both inputs are empty range, the result array is empty.
 > 2. if both inputs are non-empty and never overlaps, put both of them
 > to the result array.
 > 3. if one input is empty another one is not, then put the non-empty
 > one into the result array.

Also covered before, but if any of this still applies please let me know.

 > after applying the patch: now the catalog data seems not correct to me.
 > SELECT  a1.amopfamily
 >          ,a1.amoplefttype::regtype
 >          ,a1.amoprighttype
 >          ,a1.amopstrategy
 >          ,amoppurpose
 >          ,amopsortfamily
 >          ,amopopr
 >          ,op.oprname
 >          ,am.amname
 > FROM    pg_amop as a1 join pg_operator op on op.oid = a1.amopopr
 > join    pg_am   am on am.oid = a1.amopmethod
 > where   amoppurpose = 'p';
 > output:
 >   amopfamily | amoplefttype  | amoprighttype | amopstrategy |
 > amoppurpose | amopsortfamily | amopopr | oprname | amname
 > 
------------+---------------+---------------+--------------+-------------+----------------+---------+---------+--------
 >         2593 | box           |           603 |           31 | p
 >    |              0 |     803 | #       | gist
 >         3919 | anyrange      |          3831 |           31 | p
 >    |              0 |    3900 | *       | gist
 >         6158 | anymultirange |          4537 |           31 | p
 >    |              0 |    4394 | *       | gist
 >         3919 | anyrange      |          3831 |           32 | p
 >    |              0 |    8747 | @-      | gist
 >         6158 | anymultirange |          4537 |           32 | p
 >    |              0 |    8407 | @-      | gist
 > (5 rows)
 >
 > select  oprcode, oprname, oprleft::regtype
 > from    pg_operator opr
 > where   opr.oprname in ('#','*','@-')
 > and     oprleft = oprright
 > and     oprleft in (603,3831,4537);
 > output:
 >
 >            oprcode           | oprname |    oprleft
 > ----------------------------+---------+---------------
 >   box_intersect              | #       | box
 >   range_intersect            | *       | anyrange
 >   multirange_intersect       | *       | anymultirange
 >   range_without_portion      | @-      | anyrange
 >   multirange_without_portion | @-      | anymultirange
 > (5 rows)

This seems correct. '#' is the name of the box overlaps operator. Probably I should add a box @- 
operator too. But see below. . . .

 > should amoppurpose = 'p' is true apply to ' @-' operator?

Yes.

 > catalog-pg-amop.html:
 > `
 > amopsortfamily oid (references pg_opfamily.oid):
 > The B-tree operator family this entry sorts according to, if an
 > ordering operator; zero if a search operator
 > `
 > you should also update the above entry, the amopsortfamily is also
 > zero for "portion operator" for the newly implemented "portion
 > operator".

Okay, done.

 > v21-0006-Add-UPDATE-DELETE-FOR-PORTION-OF.patch
 >   create mode 100644 src/backend/utils/adt/period.c
 >   create mode 100644 src/include/utils/period.h
 > you should put these two files to v21-0008-Add-PERIODs.patch.
 > it's not related to that patch, it also makes people easy to review.

You're right, sorry!

On 1/8/24 16:00, jian he wrote:
 >
 > +/*
 > + * ForPortionOfClause
 > + * representation of FOR PORTION OF <period-name> FROM <ts> TO <te>
 > + * or FOR PORTION OF <period-name> (<target>)
 > + */
 > +typedef struct ForPortionOfClause
 > +{
 > + NodeTag type;
 > + char   *range_name;
 > + int range_name_location;
 > + Node   *target;
 > + Node   *target_start;
 > + Node   *target_end;
 > +} ForPortionOfClause;
 >
 > "range_name_location" can be just "location"?
 > generally most of the struct put the "location" to the last field in the struct.
 > (that's the pattern I found all over other code)

Agreed, done.

 > + if (isUpdate)
 > + {
 > + /*
 > + * Now make sure we update the start/end time of the record.
 > + * For a range col (r) this is `r = r * targetRange`.
 > + */
 > + Expr *rangeSetExpr;
 > + TargetEntry *tle;
 > +
 > + strat = RTIntersectStrategyNumber;
 > + GetOperatorFromCanonicalStrategy(opclass, InvalidOid, "intersects",
 > "FOR PORTION OF", &opid, &strat);
 > + rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
 > + (Node *) copyObject(rangeVar), targetExpr,
 > + forPortionOf->range_name_location);
 > + rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr,
 > EXPR_KIND_UPDATE_PORTION);
 > +
 > + /* Make a TLE to set the range column */
 > + result->rangeSet = NIL;
 > + tle = makeTargetEntry(rangeSetExpr, range_attno, range_name, false);
 > + result->rangeSet = lappend(result->rangeSet, tle);
 > +
 > + /* Mark the range column as requiring update permissions */
 > + target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
 > +  range_attno - FirstLowInvalidHeapAttributeNumber);
 > + }
 > + else
 > + result->rangeSet = NIL;
 > I think the name "rangeSet" is misleading, since "set" is generally
 > related to a set of records.
 > but here it's more about the "range intersect".

Okay, I can see that. I used "rangeSet" because we add it to the SET clause of the UPDATE command. 
Here I've changed it to rangeTargetList. I think this matches other code and better indicates what 
it holds. Any objections?

In the PERIOD patch we will need two TLEs here (that's why it's a List): one for the start column 
and one for the end column.

 > in ExecDelete
 > we have following code pattern:
 > ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
 > if (processReturning && resultRelInfo->ri_projectReturning)
 > {
 > ....
 > if (!table_tuple_fetch_row_version(resultRelationDesc, tupleid,
 >     SnapshotAny, slot))
 > elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
 > }
 > }
 >
 > but the ExecForPortionOfLeftovers is inside ExecDeleteEpilogue.
 > meaning even without ExecForPortionOfLeftovers, we can still call
 > table_tuple_fetch_row_version
 > also if it was *not* concurrently updated, then our current process
 > holds the lock until the ending of the transaction, i think.
 > So the following TODO is unnecessary?
 >
 > + /*
 > + * Get the range of the old pre-UPDATE/DELETE tuple,
 > + * so we can intersect it with the FOR PORTION OF target
 > + * and see if there are any "leftovers" to insert.
 > + *
 > + * We have already locked the tuple in ExecUpdate/ExecDelete
 > + * (TODO: if it was *not* concurrently updated, does
 > table_tuple_update lock the tuple itself?
 > + * I don't found the code for that yet, and maybe it depends on the AM?)
 > + * and it has passed EvalPlanQual.
 > + * Make sure we're looking at the most recent version.
 > + * Otherwise concurrent updates of the same tuple in READ COMMITTED
 > + * could insert conflicting "leftovers".
 > + */
 > + if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
 > tupleid, SnapshotAny, oldtupleSlot))
 > + elog(ERROR, "failed to fetch tuple for FOR PORTION OF");

I think you're right. According to the comments on TM_Result (returned by table_tuple_update), a 
TM_Ok indicates that the lock was acquired.

 > +/* ----------------------------------------------------------------
 > + * ExecForPortionOfLeftovers
 > + *
 > + * Insert tuples for the untouched timestamp of a row in a FOR
 > + * PORTION OF UPDATE/DELETE
 > + * ----------------------------------------------------------------
 > + */
 > +static void
 > +ExecForPortionOfLeftovers(ModifyTableContext *context,
 > +   EState *estate,
 > +   ResultRelInfo *resultRelInfo,
 > +   ItemPointer tupleid)
 >
 > maybe change the comment to
 > "Insert tuples for the not intersection of a row in a FOR PORTION OF
 > UPDATE/DELETE."

Changed to "untouched portion".

 > + deconstruct_array(DatumGetArrayTypeP(allLeftovers),
 > typcache->type_id, typcache->typlen,
 > +   typcache->typbyval, typcache->typalign, &leftovers, NULL, &nleftovers);
 > +
 > + if (nleftovers > 0)
 > + {
 > I think add something like assert nleftovers >=0 && nleftovers <= 2
 > (assume only range not multirange) would improve readability.

I added the first assert. The second is not true for non-range types.

 > +  <para>
 > +   If the table has a range column or
 > +   <link linkend="ddl-periods-application-periods"><literal>PERIOD</literal></link>,
 > +   you may supply a <literal>FOR PORTION OF</literal> clause, and
 > your delete will
 > +   only affect rows that overlap the given interval. Furthermore, if
 > a row's span
 > +   extends outside the <literal>FOR PORTION OF</literal> bounds, then
 > your delete
 > +   will only change the span within those bounds. In effect you are
 > deleting any
 > +   moment targeted by <literal>FOR PORTION OF</literal> and no moments outside.
 > +  </para>
 > +
 > +  <para>
 > +   Specifically, after <productname>PostgreSQL</productname> deletes
 > the existing row,
 > +   it will <literal>INSERT</literal>
 > +   new rows whose range or start/end column(s) receive the remaining
 > span outside
 > +   the targeted bounds, containing the original values in other columns.
 > +   There will be zero to two inserted records,
 > +   depending on whether the original span extended before the targeted
 > +   <literal>FROM</literal>, after the targeted <literal>TO</literal>,
 > both, or neither.
 > +  </para>
 > +
 > +  <para>
 > +   These secondary inserts fire <literal>INSERT</literal> triggers. First
 > +   <literal>BEFORE DELETE</literal> triggers 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>
 >
 > I think you need to wrap them into a big paragraph, otherwise they
 > lose the context?
 > please see the attached build sql-update.html.

Still TODO.

 > also I think
 > +   <link linkend="ddl-periods-application-periods"><literal>PERIOD</literal></link>,
 > should shove into Add-PERIODs.patch.
 >
 > otherwise you cannot build  Add-UPDATE-DELETE-FOR-PORTION-OF.patch
 > without all the patches.

Fixed.

 > I think the "FOR-PORTION-OF" feature is kind of independ?
 > Because, IMHO, "for portion" is a range datum interacting with another
 > single range datum, but the primary key with  "WITHOUT OVERLAPS", is
 > range datum interacting with a set of range datums.
 > now I cannot  just git apply v22-0006-Add-UPDATE-DELETE-FOR-PORTION-OF.patch.
 > That maybe would make it more difficult to get commited?

Still TODO.

On 1/8/24 21:33, jian he wrote:
 >
 > src5=# select range_without_portion(numrange(1.0,3.0,'[]'),
 > numrange(1.5,2.0,'(]'));
 >     range_without_portion
 > ---------------------------
 >   {"[1.0,1.5]","(2.0,3.0]"}
 > (1 row)
 >
 > src5=# \gdesc
 >          Column         |   Type
 > -----------------------+-----------
 >   range_without_portion | numeric[]
 > (1 row)
 >
 > src5=# \df range_without_portion
 >                                   List of functions
 >     Schema   |         Name          | Result data type | Argument data
 > types | Type
 > ------------+-----------------------+------------------+---------------------+------
 >   pg_catalog | range_without_portion | anyarray         | anyrange,
 > anyrange  | func
 > (1 row)
 >
 > so apparently, you cannot from (anyrange, anyrange) get anyarray the
 > element type is anyrange.
 > I cannot find the documented explanation in
 > https://www.postgresql.org/docs/current/extend-type-system.html#EXTEND-TYPES-POLYMORPHIC
 >
 > anyrange is POLYMORPHIC, anyarray is POLYMORPHIC,
 > but I suppose, getting an anyarray the element type is anyrange would be hard.

You're right, that is a problem.

I think the right approach is to make intersect and without_portion just be support functions, not 
operators. Then I don't need to introduce the new 'p' amop strategy at all, which seemed like a 
dubious idea anyway. Then the without_portion function can return a SETOF instead of an array.

Another idea is to add more polymorphic types, anyrangearray and anymultirangearray, but maybe that 
is too big a thing. OTOH I have wanted those same types before. I will take a stab at it.

On 1/11/24 06:44, Peter Eisentraut wrote:
 > Here is some more detailed review of the first two patches.  (I reviewed v20; I see you have also
 > posted v21, but they don't appear very different for this purpose.)
 >
 > v20-0001-Add-stratnum-GiST-support-function.patch
 >
 > * contrib/btree_gist/Makefile
 >
 > Needs corresponding meson.build updates.

Fixed.

 > * contrib/btree_gist/btree_gist--1.7--1.8.sql
 >
 > Should gist_stratnum_btree() live in contrib/btree_gist/ or in core?
 > Are there other extensions that use the btree strategy numbers for
 > gist?

Moved. None of our other contrib extensions use it. I thought it would be friendly to offer it to 
outside extensions, but maybe that is too speculative.

 > +ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD
 > +   FUNCTION  12 (varbit, varbit) gist_stratnum_btree (int2) ;
 >
 > Is there a reason for the extra space after FUNCTION here (repeated
 > throughout the file)?

Fixed.

 > +-- added in 1.4:
 >
 > What is the purpose of these "added in" comments?

I added those to help me make sure I was including every type in the extension, but I've taken them 
out here.

 > v20-0002-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch
 >
 > * contrib/btree_gist/Makefile
 >
 > Also update meson.build.

Done.

 > * contrib/btree_gist/sql/without_overlaps.sql
 >
 > Maybe also insert a few values, to verify that the constraint actually
 > does something?

Done.

 > * doc/src/sgml/ref/create_table.sgml
 >
 > Is "must have a range type" still true?  With the changes to the
 > strategy number mapping, any type with a supported operator class
 > should work?

Updated. Probably more docs to come; I want to go through them all now that we support more types.

 > * src/backend/utils/adt/ruleutils.c
 >
 > Is it actually useful to add an argument to
 > decompile_column_index_array()?  Wouldn't it be easier to just print
 > the " WITHOUT OVERLAPS" in the caller after returning from it?

Okay, done.

 > * src/include/access/gist_private.h
 >
 > The added function gistTranslateStratnum() isn't really "private" to
 > gist.  So access/gist.h would be a better place for it.

Moved.

 > Also, most other functions there appear to be named "GistSomething",
 > so a more consistent name might be GistTranslateStratnum.
 >
 > * src/include/access/stratnum.h

Changed.

 > The added StrategyIsValid() doesn't seem that useful?  Plenty of
 > existing code just compares against InvalidStrategy, and there is only
 > one caller for the new function.  I suggest to do without it.
 >
 > * src/include/commands/defrem.h

Okay, removed.

 > We are using two terms here, well-known strategy number and canonical
 > strategy number, to mean the same thing (I think?).  Let's try to
 > stick with one.  Or explain the relationship?

True. Changed everything to "well-known" which seems like a better match for what's going on.

I haven't gone through jian he's Jan 13 patch yet, but since he was also implementing Peter's 
requests I thought I should share what I have. I did this work a while ago, but I was hoping to 
finish the TODOs above first, and then we got hit with a winter storm that knocked out power. Sorry 
to cause duplicate work!

Rebased to 2f35c14cfb.

Yours,

From 9899799276158c81275d368d31d0b4823aeffa37 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 13 Dec 2023 21:17:53 -0800
Subject: [PATCH v23 1/8] Add stratnum GiST support function

This is support function 12 for the GiST AM and translates "well-known"
RT*StrategyNumber values into whatever strategy number is used by the
opclass (since no particular numbers are actually required). We will use
this to support temporal PRIMARY KEY/UNIQUE/FOREIGN KEY/FOR PORTION OF
functionality.

This commit adds two implementations, one for internal GiST opclasses
(just an identity function) and another for btree_gist opclasses. It
updates btree_gist from 1.7 to 1.8, adding the support function for all
its opclasses.
---
 contrib/btree_gist/Makefile                  |  6 +-
 contrib/btree_gist/btree_gist--1.7--1.8.sql  | 87 ++++++++++++++++++++
 contrib/btree_gist/btree_gist.c              | 30 +++++++
 contrib/btree_gist/btree_gist.control        |  2 +-
 contrib/btree_gist/expected/stratnum.out     | 13 +++
 contrib/btree_gist/meson.build               |  2 +
 contrib/btree_gist/sql/stratnum.sql          |  3 +
 doc/src/sgml/gist.sgml                       | 65 ++++++++++++++-
 doc/src/sgml/xindex.sgml                     |  8 +-
 src/backend/access/gist/gistutil.c           | 14 ++++
 src/backend/access/gist/gistvalidate.c       |  8 +-
 src/include/access/gist.h                    |  3 +-
 src/include/catalog/pg_amproc.dat            | 18 ++++
 src/include/catalog/pg_proc.dat              |  6 ++
 src/test/regress/expected/misc_functions.out | 13 +++
 src/test/regress/sql/misc_functions.sql      |  4 +
 16 files changed, 275 insertions(+), 7 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.7--1.8.sql
 create mode 100644 contrib/btree_gist/expected/stratnum.out
 create mode 100644 contrib/btree_gist/sql/stratnum.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index 073dcc745c4..a4101448935 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -33,12 +33,14 @@ EXTENSION = btree_gist
 DATA = btree_gist--1.0--1.1.sql \
        btree_gist--1.1--1.2.sql btree_gist--1.2.sql btree_gist--1.2--1.3.sql \
        btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \
-       btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql
+       btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \
+			 btree_gist--1.7--1.8.sql
 PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
-        bytea bit varbit numeric uuid not_equal enum bool partitions
+        bytea bit varbit numeric uuid not_equal enum bool partitions \
+        stratnum
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
diff --git a/contrib/btree_gist/btree_gist--1.7--1.8.sql b/contrib/btree_gist/btree_gist--1.7--1.8.sql
new file mode 100644
index 00000000000..307bfe574b0
--- /dev/null
+++ b/contrib/btree_gist/btree_gist--1.7--1.8.sql
@@ -0,0 +1,87 @@
+/* contrib/btree_gist/btree_gist--1.7--1.8.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.8'" to load this file. \quit
+
+CREATE FUNCTION gist_stratnum_btree(smallint)
+RETURNS smallint
+AS 'MODULE_PATHNAME'
+LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
+
+ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD
+	FUNCTION 12 (oid, oid) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
+	FUNCTION 12 (int2, int2) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
+	FUNCTION 12 (int4, int4) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
+	FUNCTION 12 (int8, int8) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD
+	FUNCTION 12 (float4, float4) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD
+	FUNCTION 12 (float8, float8) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD
+	FUNCTION 12 (timestamp, timestamp) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD
+	FUNCTION 12 (timestamptz, timestamptz) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_time_ops USING gist ADD
+	FUNCTION 12 (time, time) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_date_ops USING gist ADD
+	FUNCTION 12 (date, date) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD
+	FUNCTION 12 (interval, interval) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD
+	FUNCTION 12 (money, money) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD
+	FUNCTION 12 (macaddr, macaddr) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_text_ops USING gist ADD
+	FUNCTION 12 (text, text) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD
+	FUNCTION 12 (bpchar, bpchar) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD
+	FUNCTION 12 (bytea, bytea) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD
+	FUNCTION 12 (numeric, numeric) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD
+	FUNCTION 12 (bit, bit) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD
+	FUNCTION 12 (varbit, varbit) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD
+	FUNCTION 12 (inet, inet) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD
+	FUNCTION 12 (cidr, cidr) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD
+	FUNCTION 12 (timetz, timetz) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD
+	FUNCTION 12 (uuid, uuid) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD
+	FUNCTION 12 (macaddr8, macaddr8) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD
+	FUNCTION 12 (anyenum, anyenum) gist_stratnum_btree (int2) ;
+
+ALTER OPERATOR FAMILY gist_bool_ops USING gist ADD
+	FUNCTION 12 (bool, bool) gist_stratnum_btree (int2) ;
diff --git a/contrib/btree_gist/btree_gist.c b/contrib/btree_gist/btree_gist.c
index 92520aedae1..0c379efacaf 100644
--- a/contrib/btree_gist/btree_gist.c
+++ b/contrib/btree_gist/btree_gist.c
@@ -3,6 +3,7 @@
  */
 #include "postgres.h"
 
+#include "access/stratnum.h"
 #include "utils/builtins.h"
 
 PG_MODULE_MAGIC;
@@ -10,6 +11,7 @@ PG_MODULE_MAGIC;
 PG_FUNCTION_INFO_V1(gbt_decompress);
 PG_FUNCTION_INFO_V1(gbtreekey_in);
 PG_FUNCTION_INFO_V1(gbtreekey_out);
+PG_FUNCTION_INFO_V1(gist_stratnum_btree);
 
 /**************************************************
  * In/Out for keys
@@ -51,3 +53,31 @@ gbt_decompress(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_POINTER(PG_GETARG_POINTER(0));
 }
+
+/*
+ * Returns the btree number for equals, otherwise invalid.
+ *
+ * This is for GiST opclasses in btree_gist (and maybe elsewhere)
+ * that use the BT*StrategyNumber constants.
+ */
+Datum
+gist_stratnum_btree(PG_FUNCTION_ARGS)
+{
+	StrategyNumber strat = PG_GETARG_UINT16(0);
+
+	switch (strat)
+	{
+		case RTEqualStrategyNumber:
+			PG_RETURN_UINT16(BTEqualStrategyNumber);
+		case RTLessStrategyNumber:
+			PG_RETURN_UINT16(BTLessStrategyNumber);
+		case RTLessEqualStrategyNumber:
+			PG_RETURN_UINT16(BTLessEqualStrategyNumber);
+		case RTGreaterStrategyNumber:
+			PG_RETURN_UINT16(BTGreaterStrategyNumber);
+		case RTGreaterEqualStrategyNumber:
+			PG_RETURN_UINT16(BTGreaterEqualStrategyNumber);
+		default:
+			PG_RETURN_UINT16(InvalidStrategy);
+	}
+}
diff --git a/contrib/btree_gist/btree_gist.control b/contrib/btree_gist/btree_gist.control
index fa9171a80a2..abf66538f32 100644
--- a/contrib/btree_gist/btree_gist.control
+++ b/contrib/btree_gist/btree_gist.control
@@ -1,6 +1,6 @@
 # btree_gist extension
 comment = 'support for indexing common datatypes in GiST'
-default_version = '1.7'
+default_version = '1.8'
 module_pathname = '$libdir/btree_gist'
 relocatable = true
 trusted = true
diff --git a/contrib/btree_gist/expected/stratnum.out b/contrib/btree_gist/expected/stratnum.out
new file mode 100644
index 00000000000..9d80c6590d9
--- /dev/null
+++ b/contrib/btree_gist/expected/stratnum.out
@@ -0,0 +1,13 @@
+-- test stratnum support func
+SELECT gist_stratnum_btree(3::smallint);
+ gist_stratnum_btree 
+---------------------
+                   0
+(1 row)
+
+SELECT gist_stratnum_btree(18::smallint);
+ gist_stratnum_btree 
+---------------------
+                   3
+(1 row)
+
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index c88a6ac84ae..a44ce905e59 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -50,6 +50,7 @@ install_data(
   'btree_gist--1.4--1.5.sql',
   'btree_gist--1.5--1.6.sql',
   'btree_gist--1.6--1.7.sql',
+  'btree_gist--1.7--1.8.sql',
   kwargs: contrib_data_args,
 )
 
@@ -89,6 +90,7 @@ tests += {
       'enum',
       'bool',
       'partitions',
+      'stratnum',
     ],
   },
 }
diff --git a/contrib/btree_gist/sql/stratnum.sql b/contrib/btree_gist/sql/stratnum.sql
new file mode 100644
index 00000000000..f58cdbe93da
--- /dev/null
+++ b/contrib/btree_gist/sql/stratnum.sql
@@ -0,0 +1,3 @@
+-- test stratnum support func
+SELECT gist_stratnum_btree(3::smallint);
+SELECT gist_stratnum_btree(18::smallint);
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 9ac6b03e6e4..913c5cd3bba 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -272,7 +272,7 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and six that are optional.
+   <acronym>GiST</acronym> must provide, and seven that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -295,6 +295,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    user-specified parameters.
    The optional eleventh method <function>sortsupport</function> is used to
    speed up building a <acronym>GiST</acronym> index.
+   The optional twelfth method <function>stratnum</function> is used to translate
+   well-known <literal>RT*StrategyNumbers</literal> (from
+   <filename>src/include/access/stratnum.h</filename>) into strategy numbers
+   used by the opclass.
  </para>
 
  <variablelist>
@@ -1169,6 +1173,65 @@ my_sortsupport(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><function>stratnum</function></term>
+     <listitem>
+      <para>
+       Given an <literal>RT*StrategyNumber</literal> value from
+       <filename>src/include/access/stratnum.h</filename>,
+       returns a strategy number used by this opclass for matching functionality.
+       The function should return <literal>InvalidStrategy</literal>
+       if the opclass has no matching strategy,
+       or at least a strategy number that isn't used by this opclass.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this:
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_stratnum(integer)
+RETURNS integer
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this skeleton:
+
+<programlisting>
+PG_FUNCTION_INFO_V1(my_stratnum);
+
+Datum
+my_stratnum(PG_FUNCTION_ARGS)
+{
+    StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(1);
+    StrategyNumber ret = InvalidStrategy;
+
+    switch (strategy)
+    {
+        case RTEqualStrategyNumber:
+            ret = BTEqualStrategyNumber;
+    }
+
+    PG_RETURN_UINT16(ret);
+}
+</programlisting>
+      </para>
+
+      <para>
+       One translation function is provided by Postgres:
+       <literal>gist_stratnum_identity</literal> is for opclasses that
+       already use the <literal>RT*StrategyNumber</literal> constants.
+       It returns whatever is passed to it.
+       The <literal>btree_gist</literal> extension defines a second
+       translation function, <literal>gist_stratnum_btree</literal>,
+       for opclasses that use the <literal>BT*StrategyNumber</literal>
+       constants.
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aac4..8c4ede31057 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
-   GiST indexes have eleven support functions, six of which are optional,
+   GiST indexes have twelve support functions, seven of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -590,6 +590,12 @@
         (optional)</entry>
        <entry>11</entry>
       </row>
+      <row>
+       <entry><function>stratnum</function></entry>
+       <entry>translate well-known strategy numbers to ones
+        used by the opclass (optional)</entry>
+       <entry>12</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index dddc08893a1..d4d08bd118f 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -23,6 +23,7 @@
 #include "storage/indexfsm.h"
 #include "storage/lmgr.h"
 #include "utils/float.h"
+#include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -1056,3 +1057,16 @@ gistGetFakeLSN(Relation rel)
 		return GetFakeLSNForUnloggedRel();
 	}
 }
+
+/*
+ * Returns the same number that was received.
+ *
+ * This is for GiST opclasses that use the RT*StrategyNumber constants.
+ */
+Datum
+gist_stratnum_identity(PG_FUNCTION_ARGS)
+{
+	StrategyNumber strat = PG_GETARG_UINT16(0);
+
+	PG_RETURN_UINT16(strat);
+}
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 7e2a715200a..698e01ed2f7 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -147,6 +147,10 @@ gistvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
 											1, 1, INTERNALOID);
 				break;
+			case GIST_STRATNUM_PROC:
+				ok = check_amproc_signature(procform->amproc, INT2OID, true,
+											1, 1, INT2OID);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -267,7 +271,8 @@ gistvalidate(Oid opclassoid)
 			continue;			/* got it */
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
-			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC)
+			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
+			i == GIST_STRATNUM_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -339,6 +344,7 @@ gistadjustmembers(Oid opfamilyoid,
 			case GIST_FETCH_PROC:
 			case GIST_OPTIONS_PROC:
 			case GIST_SORTSUPPORT_PROC:
+			case GIST_STRATNUM_PROC:
 				/* Optional, so force it to be a soft family dependency */
 				op->ref_is_hard = false;
 				op->ref_is_family = true;
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index c6dcd6a90dd..e7ced18a5ba 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -38,7 +38,8 @@
 #define GIST_FETCH_PROC					9
 #define GIST_OPTIONS_PROC				10
 #define GIST_SORTSUPPORT_PROC			11
-#define GISTNProcs					11
+#define GIST_STRATNUM_PROC				12
+#define GISTNProcs					12
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index f639c3a6a51..352558c1f06 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -507,6 +507,9 @@
   amprocrighttype => 'box', amprocnum => '7', amproc => 'gist_box_same' },
 { amprocfamily => 'gist/box_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '8', amproc => 'gist_box_distance' },
+{ amprocfamily => 'gist/box_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '1',
   amproc => 'gist_poly_consistent' },
@@ -526,6 +529,9 @@
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '8',
   amproc => 'gist_poly_distance' },
+{ amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
+  amprocrighttype => 'polygon', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 { amprocfamily => 'gist/circle_ops', amproclefttype => 'circle',
   amprocrighttype => 'circle', amprocnum => '1',
   amproc => 'gist_circle_consistent' },
@@ -544,6 +550,9 @@
 { amprocfamily => 'gist/circle_ops', amproclefttype => 'circle',
   amprocrighttype => 'circle', amprocnum => '8',
   amproc => 'gist_circle_distance' },
+{ amprocfamily => 'gist/circle_ops', amproclefttype => 'circle',
+  amprocrighttype => 'circle', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
   amprocrighttype => 'tsvector', amprocnum => '1',
   amproc => 'gtsvector_consistent(internal,tsvector,int2,oid,internal)' },
@@ -598,6 +607,9 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '7',
   amproc => 'range_gist_same' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -614,6 +626,9 @@
   amprocrighttype => 'inet', amprocnum => '7', amproc => 'inet_gist_same' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '9', amproc => 'inet_gist_fetch' },
+{ amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
+  amprocrighttype => 'inet', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '1',
   amproc => 'multirange_gist_consistent' },
@@ -632,6 +647,9 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '7',
   amproc => 'range_gist_same' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '12',
+  amproc => 'gist_stratnum_identity' },
 
 # gin
 { amprocfamily => 'gin/array_ops', amproclefttype => 'anyarray',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 58811a6530b..a0277e57c7d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12152,4 +12152,10 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# GiST stratnum implementations
+{ oid => '8047', descr => 'GiST support',
+  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proargtypes => 'int2',
+  prosrc => 'gist_stratnum_identity' },
+
 ]
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 93021340773..5b103b0c1b2 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -670,3 +670,16 @@ FROM pg_walfile_name_offset('0/0'::pg_lsn + :segment_size - 1),
               0 | t
 (1 row)
 
+-- test stratnum support funcs
+SELECT gist_stratnum_identity(3::smallint);
+ gist_stratnum_identity 
+------------------------
+                      3
+(1 row)
+
+SELECT gist_stratnum_identity(18::smallint);
+ gist_stratnum_identity 
+------------------------
+                     18
+(1 row)
+
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index d3dc591173d..9e40e606859 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -250,3 +250,7 @@ FROM pg_walfile_name_offset('0/0'::pg_lsn + :segment_size + 1),
 SELECT segment_number, file_offset = :segment_size - 1
 FROM pg_walfile_name_offset('0/0'::pg_lsn + :segment_size - 1),
      pg_split_walfile_name(file_name);
+
+-- test stratnum support funcs
+SELECT gist_stratnum_identity(3::smallint);
+SELECT gist_stratnum_identity(18::smallint);
-- 
2.42.0

From 1db8cb9da5859b9b362f3e4a10bc266c12c43494 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v23 2/8] Add temporal PRIMARY KEY and UNIQUE constraints

- Added WITHOUT OVERLAPS to the bison grammar. We permit only range
  columns, not yet PERIODs (which we'll add soon).
- Temporal PRIMARY KEYs and UNIQUE constraints are backed by GiST
  indexes instead of B-tree indexes, since they are essentially
  exclusion constraints with = for the scalar parts of the key and &&
  for the temporal part.
- Added pg_constraint.contemporal to say whether a constraint is a
  temporal constraint.
- Added docs and tests.
- Added pg_dump support.
---
 contrib/btree_gist/Makefile                   |   2 +-
 .../btree_gist/expected/without_overlaps.out  |  44 ++
 contrib/btree_gist/meson.build                |   1 +
 contrib/btree_gist/sql/without_overlaps.sql   |  25 ++
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/gist.sgml                        |  18 +-
 doc/src/sgml/ref/create_table.sgml            |  43 +-
 src/backend/access/gist/gistutil.c            |  29 ++
 src/backend/catalog/heap.c                    |   2 +
 src/backend/catalog/index.c                   |   4 +
 src/backend/catalog/pg_constraint.c           |   2 +
 src/backend/commands/indexcmds.c              | 157 ++++++-
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/commands/trigger.c                |   1 +
 src/backend/commands/typecmds.c               |   1 +
 src/backend/nodes/outfuncs.c                  |   2 +
 src/backend/nodes/readfuncs.c                 |   2 +
 src/backend/parser/gram.y                     |  29 +-
 src/backend/parser/parse_utilcmd.c            |  28 +-
 src/backend/utils/adt/ruleutils.c             |   2 +
 src/backend/utils/cache/relcache.c            |  18 +-
 src/bin/pg_dump/pg_dump.c                     |  16 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  40 ++
 src/bin/psql/describe.c                       |  12 +-
 src/include/access/gist.h                     |   3 +
 src/include/catalog/index.h                   |   1 +
 src/include/catalog/pg_constraint.h           |  10 +-
 src/include/commands/defrem.h                 |   7 +-
 src/include/nodes/parsenodes.h                |   2 +
 .../regress/expected/without_overlaps.out     | 395 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/without_overlaps.sql     | 290 +++++++++++++
 33 files changed, 1155 insertions(+), 50 deletions(-)
 create mode 100644 contrib/btree_gist/expected/without_overlaps.out
 create mode 100644 contrib/btree_gist/sql/without_overlaps.sql
 create mode 100644 src/test/regress/expected/without_overlaps.out
 create mode 100644 src/test/regress/sql/without_overlaps.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index a4101448935..9a745757855 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -40,7 +40,7 @@ PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"
 REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \
         time timetz date interval macaddr macaddr8 inet cidr text varchar char \
         bytea bit varbit numeric uuid not_equal enum bool partitions \
-        stratnum
+        stratnum without_overlaps
 
 SHLIB_LINK += $(filter -lm, $(LIBS))
 
diff --git a/contrib/btree_gist/expected/without_overlaps.out b/contrib/btree_gist/expected/without_overlaps.out
new file mode 100644
index 00000000000..be52c522e89
--- /dev/null
+++ b/contrib/btree_gist/expected/without_overlaps.out
@@ -0,0 +1,44 @@
+-- Core must test WITHOUT OVERLAPS
+-- with an int4range + daterange,
+-- so here we do some simple tests
+-- to make sure int + daterange works too,
+-- since that is the expected use-case.
+CREATE TABLE temporal_rng (
+  id integer,
+  valid_at daterange,
+  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+              Table "public.temporal_rng"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | integer   |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+            pg_get_constraintdef             
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+                                pg_get_indexdef                                
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
+(1 row)
+
+INSERT INTO temporal_rng VALUES
+  (1, '[2000-01-01,2001-01-01)');
+-- same key, doesn't overlap:
+INSERT INTO temporal_rng VALUES
+  (1, '[2001-01-01,2002-01-01)');
+-- overlaps but different key:
+INSERT INTO temporal_rng VALUES
+  (2, '[2000-01-01,2001-01-01)');
+-- should fail:
+INSERT INTO temporal_rng VALUES
+  (1, '[2000-06-01,2001-01-01)');
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL:  Key (id, valid_at)=(1, [06-01-2000,01-01-2001)) conflicts with existing key (id, valid_at)=(1, [01-01-2000,01-01-2001)).
diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build
index a44ce905e59..73b1bbf52a6 100644
--- a/contrib/btree_gist/meson.build
+++ b/contrib/btree_gist/meson.build
@@ -91,6 +91,7 @@ tests += {
       'bool',
       'partitions',
       'stratnum',
+      'without_overlaps',
     ],
   },
 }
diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql
new file mode 100644
index 00000000000..433c609ab22
--- /dev/null
+++ b/contrib/btree_gist/sql/without_overlaps.sql
@@ -0,0 +1,25 @@
+-- Core must test WITHOUT OVERLAPS
+-- with an int4range + daterange,
+-- so here we do some simple tests
+-- to make sure int + daterange works too,
+-- since that is the expected use-case.
+CREATE TABLE temporal_rng (
+  id integer,
+  valid_at daterange,
+  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+
+INSERT INTO temporal_rng VALUES
+  (1, '[2000-01-01,2001-01-01)');
+-- same key, doesn't overlap:
+INSERT INTO temporal_rng VALUES
+  (1, '[2001-01-01,2002-01-01)');
+-- overlaps but different key:
+INSERT INTO temporal_rng VALUES
+  (2, '[2000-01-01,2001-01-01)');
+-- should fail:
+INSERT INTO temporal_rng VALUES
+  (1, '[2000-06-01,2001-01-01)');
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c15d861e823..16b94461b2c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2709,6 +2709,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>conwithoutoverlaps</structfield> <type>bool</type>
+      </para>
+      <para>
+       This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
+       (for primary keys and unique constraints).
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>conkey</structfield> <type>int2[]</type>
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 913c5cd3bba..86be7c96256 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -298,7 +298,8 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    The optional twelfth method <function>stratnum</function> is used to translate
    well-known <literal>RT*StrategyNumbers</literal> (from
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
-   used by the opclass.
+   used by the opclass. This lets the core code look up operators for temporal
+   constraint indexes.
  </para>
 
  <variablelist>
@@ -1184,6 +1185,21 @@ my_sortsupport(PG_FUNCTION_ARGS)
        if the opclass has no matching strategy,
        or at least a strategy number that isn't used by this opclass.
       </para>
+      <para>
+       This is used for temporal index constraints
+       (i.e. <literal>PRIMARY KEY</literal> and <literal>UNIQUE</literal>).
+      </para>
+      <para>
+       If the opclass provides this function and it returns results
+       for <literal>RTEqualStrategyNumber</literal>, it can be used
+       in the non-<literal>WITHOUT OVERLAPS</literal> part(s) of an
+       index constraint.
+      </para>
+      <para>
+       If it returns results for <literal>RTOverlapStrategyNumber</literal>,
+       the opclass can be used in the <literal>WITHOUT OVERLAPS</literal>
+       part of an index constraint.
+      </para>
 
       <para>
        The <acronym>SQL</acronym> declaration of the function must look like
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e04a0692c4e..278af2e3869 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -78,8 +78,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
-  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
-  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
   FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
@@ -965,7 +965,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
    <varlistentry id="sql-createtable-parms-unique">
     <term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ]</literal> (column constraint)</term>
-    <term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
+    <term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] )</literal>
     <optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
 
     <listitem>
@@ -979,6 +979,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       of these columns.
      </para>
 
+     <para>
+      If the <literal>WITHOUT OVERLAPS</literal> option is specified for the
+      last column, then that column is checked for overlaps instead of
+      equality.  In that case, the other columns of the constraint will allow
+      duplicates so long as the duplicates don't overlap in the
+      <literal>WITHOUT OVERLAPS</literal> column.  (This is sometimes called a
+      temporal key, if the column is a range of dates or timestamps, but
+      PostgreSQL allows ranges over any base type.)  In effect, such a
+      constraint is enforced with an <literal>EXCLUDE</literal> constraint
+      rather than a <literal>UNIQUE</literal> constraint.  So for example
+      <literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves like
+      <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
+      &amp;&amp;)</literal>.  The <literal>WITHOUT OVERLAPS</literal> column
+      must have a range or multirange type. (Technically any type is allowed
+      whose default GiST opclass includes an overlaps operator. See the
+      <literal>stratnum</literal> support function under
+      <xref linkend="gist-extensibility"/> for details.)
+      The non-<literal>WITHOUT OVERLAPS</literal>
+      columns of the constraint can be any type that can be compared for
+      equality in a GiST index. By default only range types are supported, but
+      you can use other types by adding the <xref linkend="btree-gist"/>
+      extension (which is the expected way to use this feature).
+     </para>
+
      <para>
       For the purpose of a unique constraint, null values are not
       considered equal, unless <literal>NULLS NOT DISTINCT</literal> is
@@ -1001,7 +1025,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
      <para>
       Adding a unique constraint will automatically create a unique btree
-      index on the column or group of columns used in the constraint.
+      index on the column or group of columns used in the constraint. But if
+      the constraint includes a <literal>WITHOUT OVERLAPS</literal> clause,
+      it will use a GiST index.
      </para>
 
      <para>
@@ -1019,7 +1045,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
    <varlistentry id="sql-createtable-parms-primary-key">
     <term><literal>PRIMARY KEY</literal> (column constraint)</term>
-    <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
+    <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] )</literal>
     <optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
     <listitem>
      <para>
@@ -1052,9 +1078,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
-      Adding a <literal>PRIMARY KEY</literal> constraint will automatically
-      create a unique btree index on the column or group of columns used in the
-      constraint.
+      As with a <literal>UNIQUE</literal> constraint, adding a
+      <literal>PRIMARY KEY</literal> constraint will automatically create a
+      unique btree index, or GiST if <literal>WITHOUT OVERLAPS</literal> was
+      specified, on the column or group of columns used in the constraint.
      </para>
 
      <para>
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index d4d08bd118f..ff4eed04aec 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1070,3 +1070,32 @@ gist_stratnum_identity(PG_FUNCTION_ARGS)
 
 	PG_RETURN_UINT16(strat);
 }
+
+/*
+ * Returns the opclass's private stratnum used for the given strategy.
+ *
+ * Calls the opclass's GIST_STRATNUM_PROC support function, if any,
+ * and returns the result.
+ * Returns InvalidStrategy if the function is not defined.
+ */
+StrategyNumber
+GistTranslateStratnum(Oid opclass, StrategyNumber strat)
+{
+	Oid	opfamily;
+	Oid	opcintype;
+	Oid	funcid;
+	Datum result;
+
+	/* Look up the opclass family and input datatype. */
+	if (!get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+		return InvalidStrategy;
+
+	/* Check whether the function is provided. */
+	funcid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_STRATNUM_PROC);
+	if (!OidIsValid(funcid))
+		return InvalidStrategy;
+
+	/* Ask the translation function */
+	result = OidFunctionCall1Coll(funcid, InvalidOid, UInt16GetDatum(strat));
+	return DatumGetUInt16(result);
+}
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 45a71081d42..c73f7bcd011 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 							  is_local, /* conislocal */
 							  inhcount, /* coninhcount */
 							  is_no_inherit,	/* connoinherit */
+							  false,	/* conwithoutoverlaps */
 							  is_internal); /* internally constructed? */
 
 	pfree(ccbin);
@@ -2191,6 +2192,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
+							  false,	/* conwithoutoverlaps */
 							  false);
 	return constrOid;
 }
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index fbef3d5382d..5c490684ce1 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1904,6 +1904,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId)
  *		INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row
  *		INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies
  *			of index on table's columns
+ *		INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS: constraint uses WITHOUT OVERLAPS
  * allow_system_table_mods: allow table to be a system catalog
  * is_internal: index is constructed due to internal process
  */
@@ -1927,11 +1928,13 @@ index_constraint_create(Relation heapRelation,
 	bool		mark_as_primary;
 	bool		islocal;
 	bool		noinherit;
+	bool		is_without_overlaps;
 	int			inhcount;
 
 	deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0;
 	initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0;
 	mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0;
+	is_without_overlaps = (constr_flags & INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS) != 0;
 
 	/* constraint creation support doesn't work while bootstrapping */
 	Assert(!IsBootstrapProcessingMode());
@@ -2008,6 +2011,7 @@ index_constraint_create(Relation heapRelation,
 								   islocal,
 								   inhcount,
 								   noinherit,
+								   is_without_overlaps,
 								   is_internal);
 
 	/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index a0232aa1bbf..0a95608179d 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -78,6 +78,7 @@ CreateConstraintEntry(const char *constraintName,
 					  bool conIsLocal,
 					  int conInhCount,
 					  bool conNoInherit,
+					  bool conWithoutOverlaps,
 					  bool is_internal)
 {
 	Relation	conDesc;
@@ -193,6 +194,7 @@ CreateConstraintEntry(const char *constraintName,
 	values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal);
 	values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
 	values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
+	values[Anum_pg_constraint_conwithoutoverlaps - 1] = BoolGetDatum(conWithoutOverlaps);
 
 	if (conkeyArray)
 		values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 340248a3f29..52f3194791d 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "access/amapi.h"
+#include "access/gist.h"
 #include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/reloptions.h"
@@ -86,6 +87,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
 							  Oid accessMethodId,
 							  bool amcanorder,
 							  bool isconstraint,
+							  bool iswithoutoverlaps,
 							  Oid ddl_userid,
 							  int ddl_sec_context,
 							  int *ddl_save_nestlevel);
@@ -144,6 +146,7 @@ typedef struct ReindexErrorInfo
  *		to index on.
  * 'exclusionOpNames': list of names of exclusion-constraint operators,
  *		or NIL if not an exclusion constraint.
+ * 'isWithoutOverlaps': true iff this index has a WITHOUT OVERLAPS clause.
  *
  * This is tailored to the needs of ALTER TABLE ALTER TYPE, which recreates
  * any indexes that depended on a changing column from their pg_get_indexdef
@@ -173,7 +176,8 @@ bool
 CheckIndexCompatible(Oid oldId,
 					 const char *accessMethodName,
 					 const List *attributeList,
-					 const List *exclusionOpNames)
+					 const List *exclusionOpNames,
+					 bool isWithoutOverlaps)
 {
 	bool		isconstraint;
 	Oid		   *typeIds;
@@ -248,8 +252,8 @@ CheckIndexCompatible(Oid oldId,
 					  coloptions, attributeList,
 					  exclusionOpNames, relationId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, isconstraint, InvalidOid, 0, NULL);
-
+					  amcanorder, isconstraint, isWithoutOverlaps, InvalidOid,
+					  0, NULL);
 
 	/* Get the soon-obsolete pg_index tuple. */
 	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -559,6 +563,7 @@ DefineIndex(Oid tableId,
 	bool		amcanorder;
 	bool		amissummarizing;
 	amoptions_function amoptions;
+	bool		exclusion;
 	bool		partitioned;
 	bool		safe_index;
 	Datum		reloptions;
@@ -677,6 +682,12 @@ DefineIndex(Oid tableId,
 
 	namespaceId = RelationGetNamespace(rel);
 
+	/*
+	 * It has exclusion constraint behavior if it's an EXCLUDE constraint
+	 * or a temporal PRIMARY KEY/UNIQUE constraint
+	 */
+	exclusion = stmt->excludeOpNames || stmt->iswithoutoverlaps;
+
 	/* Ensure that it makes sense to index this kind of relation */
 	switch (rel->rd_rel->relkind)
 	{
@@ -845,7 +856,7 @@ DefineIndex(Oid tableId,
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
 								 accessMethodId);
 
-	if (stmt->unique && !amRoutine->amcanunique)
+	if (stmt->unique && !stmt->iswithoutoverlaps && !amRoutine->amcanunique)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support unique indexes",
@@ -860,7 +871,7 @@ DefineIndex(Oid tableId,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support multicolumn indexes",
 						accessMethodName)));
-	if (stmt->excludeOpNames && amRoutine->amgettuple == NULL)
+	if (exclusion && amRoutine->amgettuple == NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support exclusion constraints",
@@ -913,8 +924,9 @@ DefineIndex(Oid tableId,
 					  coloptions, allIndexParams,
 					  stmt->excludeOpNames, tableId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, stmt->isconstraint, root_save_userid,
-					  root_save_sec_context, &root_save_nestlevel);
+					  amcanorder, stmt->isconstraint, stmt->iswithoutoverlaps,
+					  root_save_userid, root_save_sec_context,
+					  &root_save_nestlevel);
 
 	/*
 	 * Extra checks when creating a PRIMARY KEY index.
@@ -932,7 +944,7 @@ DefineIndex(Oid tableId,
 	 * We could lift this limitation if we had global indexes, but those have
 	 * their own problems, so this is a useful feature combination.
 	 */
-	if (partitioned && (stmt->unique || stmt->excludeOpNames))
+	if (partitioned && (stmt->unique || exclusion))
 	{
 		PartitionKey key = RelationGetPartitionKey(rel);
 		const char *constraint_type;
@@ -986,10 +998,10 @@ DefineIndex(Oid tableId,
 			 * associated with index columns, too.  We know what to do with
 			 * btree opclasses; if there are ever any other index types that
 			 * support unique indexes, this logic will need extension. But if
-			 * we have an exclusion constraint, it already knows the
-			 * operators, so we don't have to infer them.
+			 * we have an exclusion constraint (or a temporal PK), it already
+			 * knows the operators, so we don't have to infer them.
 			 */
-			if (stmt->unique && accessMethodId != BTREE_AM_OID)
+			if (stmt->unique && !stmt->iswithoutoverlaps && accessMethodId != BTREE_AM_OID)
 				ereport(ERROR,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("cannot match partition key to an index using access method \"%s\"",
@@ -1028,12 +1040,12 @@ DefineIndex(Oid tableId,
 					{
 						Oid			idx_eqop = InvalidOid;
 
-						if (stmt->unique)
+						if (stmt->unique && !stmt->iswithoutoverlaps)
 							idx_eqop = get_opfamily_member(idx_opfamily,
 														   idx_opcintype,
 														   idx_opcintype,
 														   BTEqualStrategyNumber);
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 							idx_eqop = indexInfo->ii_ExclusionOps[j];
 						Assert(idx_eqop);
 
@@ -1042,7 +1054,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1186,6 +1198,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+	if (stmt->iswithoutoverlaps)
+		constr_flags |= INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1850,6 +1864,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool iswithoutoverlaps,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1873,6 +1888,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;
 
+	/* exclusionOpNames can be non-NIL if we are creating a partition */
+	if (iswithoutoverlaps && exclusionOpNames == NIL)
+	{
+		indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
+		indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
+		indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols);
+	}
+
 	if (OidIsValid(ddl_userid))
 		GetUserIdAndSecContext(&save_userid, &save_sec_context);
 
@@ -2149,6 +2172,31 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 			indexInfo->ii_ExclusionStrats[attn] = strat;
 			nextExclOp = lnext(exclusionOpNames, nextExclOp);
 		}
+		else if (iswithoutoverlaps)
+		{
+			StrategyNumber strat;
+			Oid opid;
+			char *opname;
+
+			if (attn == nkeycols - 1)
+			{
+				strat = RTOverlapStrategyNumber;
+				opname = "overlaps";
+			}
+			else
+			{
+				strat = RTEqualStrategyNumber;
+				opname = "equals";
+			}
+			GetOperatorFromWellKnownStrategy(opclassOids[attn],
+											 atttype,
+											 opname,
+											 &opid,
+											 &strat);
+			indexInfo->ii_ExclusionOps[attn] = opid;
+			indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
+			indexInfo->ii_ExclusionStrats[attn] = strat;
+		}
 
 		/*
 		 * Set up the per-column options (indoption field).  For now, this is
@@ -2379,6 +2427,87 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
 	return InvalidOid;
 }
 
+/*
+ * GetOperatorFromWellKnownStrategy
+ *
+ * opclass - the opclass to use
+ * atttype - the type to ask about
+ * opname - used to build error messages
+ * opid - holds the operator we found
+ * strat - holds the input and output strategy number
+ *
+ * Finds an operator from a "well-known" strategy number.
+ * This is used for temporal index constraints
+ * (and other temporal features)
+ * to look up equality and overlaps operators, since
+ * the strategy numbers for non-btree indexams need not
+ * follow any fixed scheme. We ask an opclass support
+ * function to translate from the well-known number
+ * to the internal value. If the function isn't defined
+ * or it gives no result, we return InvalidStrategy.
+ */
+void
+GetOperatorFromWellKnownStrategy(Oid opclass,
+								 Oid atttype,
+								 const char *opname,
+								 Oid *opid,
+								 StrategyNumber *strat)
+{
+	Oid opfamily;
+	Oid opcintype;
+	StrategyNumber opstrat = *strat;
+
+	*opid = InvalidOid;
+
+	if (get_opclass_opfamily_and_input_type(opclass,
+											&opfamily,
+											&opcintype))
+	{
+		/*
+		 * Ask the opclass to translate to its internal stratnum
+		 *
+		 * For now we only need GiST support, but this could support
+		 * other indexams if we wanted.
+		 */
+		*strat = GistTranslateStratnum(opclass, opstrat);
+		if (*strat == InvalidStrategy)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+					 errdetail("Could not translate strategy number %u for opclass %d.",
+						 opstrat, opclass),
+					 errhint("Define a stratnum support function for your GiST opclass.")));
+
+		*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
+	}
+
+	if (!OidIsValid(*opid))
+	{
+		HeapTuple	opftuple;
+		Form_pg_opfamily opfform;
+
+		/*
+		 * attribute->opclass might not explicitly name the opfamily,
+		 * so fetch the name of the selected opfamily for use in the
+		 * error message.
+		 */
+		opftuple = SearchSysCache1(OPFAMILYOID,
+								   ObjectIdGetDatum(opfamily));
+		if (!HeapTupleIsValid(opftuple))
+			elog(ERROR, "cache lookup failed for opfamily %u",
+				 opfamily);
+		opfform = (Form_pg_opfamily) GETSTRUCT(opftuple);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+				 errdetail("There must be an %s operator within opfamily \"%s\" for type \"%s\".",
+						   opname,
+						   NameStr(opfform->opfname),
+						   format_type_be(atttype))));
+	}
+}
+
 /*
  *	makeObjectName()
  *
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2a56a4357c9..e8622b7a8e5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10336,6 +10336,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
+									  false,	/* conWithoutOverlaps */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10634,6 +10635,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -11139,6 +11141,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conWithoutOverlaps */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -14552,7 +14555,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 	if (CheckIndexCompatible(oldId,
 							 stmt->accessMethod,
 							 stmt->indexParams,
-							 stmt->excludeOpNames))
+							 stmt->excludeOpNames,
+							 stmt->iswithoutoverlaps))
 	{
 		Relation	irel = index_open(oldId, NoLock);
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0880ca51fb2..a7c92416f56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 											  true, /* islocal */
 											  0,	/* inhcount */
 											  true, /* noinherit */
+											  false, /* conwithoutoverlaps */
 											  isInternal);	/* is_internal */
 	}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 27cc186c16f..a400fb39f67 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3544,6 +3544,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  true, /* is local */
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
+							  false,	/* conwithoutoverlaps */
 							  false);	/* is_internal */
 	if (constrAddr)
 		ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 296ba845187..03f67b68506 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -756,6 +756,7 @@ _outConstraint(StringInfo str, const Constraint *node)
 		case CONSTR_PRIMARY:
 			appendStringInfoString(str, "PRIMARY_KEY");
 			WRITE_NODE_FIELD(keys);
+			WRITE_BOOL_FIELD(without_overlaps);
 			WRITE_NODE_FIELD(including);
 			WRITE_NODE_FIELD(options);
 			WRITE_STRING_FIELD(indexname);
@@ -768,6 +769,7 @@ _outConstraint(StringInfo str, const Constraint *node)
 			appendStringInfoString(str, "UNIQUE");
 			WRITE_BOOL_FIELD(nulls_not_distinct);
 			WRITE_NODE_FIELD(keys);
+			WRITE_BOOL_FIELD(without_overlaps);
 			WRITE_NODE_FIELD(including);
 			WRITE_NODE_FIELD(options);
 			WRITE_STRING_FIELD(indexname);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 1624b345812..cfb552fde74 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -427,6 +427,7 @@ _readConstraint(void)
 
 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
+			READ_BOOL_FIELD(without_overlaps);
 			READ_NODE_FIELD(including);
 			READ_NODE_FIELD(options);
 			READ_STRING_FIELD(indexname);
@@ -438,6 +439,7 @@ _readConstraint(void)
 		case CONSTR_UNIQUE:
 			READ_BOOL_FIELD(nulls_not_distinct);
 			READ_NODE_FIELD(keys);
+			READ_BOOL_FIELD(without_overlaps);
 			READ_NODE_FIELD(including);
 			READ_NODE_FIELD(options);
 			READ_STRING_FIELD(indexname);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3460fea56ba..130f7fc7c3f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -529,7 +529,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				columnref in_expr having_clause func_table xmltable array_expr
 				OptWhereClause operator_def_arg
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality
+%type <boolean> opt_ordinality opt_without_overlaps
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4133,7 +4133,7 @@ ConstraintElem:
 					n->initially_valid = true;
 					$$ = (Node *) n;
 				}
-			| UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+			| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4142,11 +4142,12 @@ ConstraintElem:
 					n->location = @1;
 					n->nulls_not_distinct = !$2;
 					n->keys = $4;
-					n->including = $6;
-					n->options = $7;
+					n->without_overlaps = $5;
+					n->including = $7;
+					n->options = $8;
 					n->indexname = NULL;
-					n->indexspace = $8;
-					processCASbits($9, @9, "UNIQUE",
+					n->indexspace = $9;
+					processCASbits($10, @10, "UNIQUE",
 								   &n->deferrable, &n->initdeferred, NULL,
 								   NULL, yyscanner);
 					$$ = (Node *) n;
@@ -4167,7 +4168,7 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
-			| PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+			| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4175,11 +4176,12 @@ ConstraintElem:
 					n->contype = CONSTR_PRIMARY;
 					n->location = @1;
 					n->keys = $4;
-					n->including = $6;
-					n->options = $7;
+					n->without_overlaps = $5;
+					n->including = $7;
+					n->options = $8;
 					n->indexname = NULL;
-					n->indexspace = $8;
-					processCASbits($9, @9, "PRIMARY KEY",
+					n->indexspace = $9;
+					processCASbits($10, @10, "PRIMARY KEY",
 								   &n->deferrable, &n->initdeferred, NULL,
 								   NULL, yyscanner);
 					$$ = (Node *) n;
@@ -4247,6 +4249,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 			| /* EMPTY */							{  $$ = false; }
 		;
 
+opt_without_overlaps:
+			WITHOUT OVERLAPS						{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+	;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index b2d1fa9d0d6..0772a0210e7 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1716,6 +1716,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	index->unique = idxrec->indisunique;
 	index->nulls_not_distinct = idxrec->indnullsnotdistinct;
 	index->primary = idxrec->indisprimary;
+	index->iswithoutoverlaps = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
 	index->transformed = true;	/* don't need transformIndexStmt */
 	index->concurrent = false;
 	index->if_not_exists = false;
@@ -1765,7 +1766,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			nElems;
 				int			i;
 
-				Assert(conrec->contype == CONSTRAINT_EXCLUSION);
+				Assert(conrec->contype == CONSTRAINT_EXCLUSION ||
+						(index->iswithoutoverlaps &&
+						 (conrec->contype == CONSTRAINT_PRIMARY || conrec->contype == CONSTRAINT_UNIQUE)));
 				/* Extract operator OIDs from the pg_constraint tuple */
 				datum = SysCacheGetAttrNotNull(CONSTROID, ht_constr,
 											   Anum_pg_constraint_conexclop);
@@ -2305,6 +2308,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	}
 	index->nulls_not_distinct = constraint->nulls_not_distinct;
 	index->isconstraint = true;
+	index->iswithoutoverlaps = constraint->without_overlaps;
 	index->deferrable = constraint->deferrable;
 	index->initdeferred = constraint->initdeferred;
 
@@ -2397,6 +2401,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 					 errmsg("index \"%s\" is not valid", index_name),
 					 parser_errposition(cxt->pstate, constraint->location)));
 
+		/*
+		 * Today we forbid non-unique indexes, but we could permit GiST
+		 * indexes whose last entry is a range type and use that to create a
+		 * WITHOUT OVERLAPS constraint (i.e. a temporal constraint).
+		 */
 		if (!index_form->indisunique)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -2673,6 +2682,23 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				notnullcmds = lappend(notnullcmds, notnullcmd);
 			}
 		}
+
+		if (constraint->without_overlaps)
+		{
+			/*
+			 * This enforces that there is at least one equality column besides
+			 * the WITHOUT OVERLAPS columns.  This is per SQL standard.  XXX
+			 * Do we need this?
+			 */
+			if (list_length(constraint->keys) < 2)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("constraint using WITHOUT OVERLAPS needs at least two columns"));
+
+			/* WITHOUT OVERLAPS requires a GiST index */
+			index->accessMethod = "gist";
+		}
+
 	}
 
 	/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0b2a164057b..b625f471a84 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2380,6 +2380,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 											 Anum_pg_constraint_conkey);
 
 				keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+				if (conForm->conwithoutoverlaps)
+					appendStringInfoString(&buf, " WITHOUT OVERLAPS");
 
 				appendStringInfoChar(&buf, ')');
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 20273f0be16..66e19b9f6da 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5568,11 +5568,14 @@ RelationGetIdentityKeyBitmap(Relation relation)
 /*
  * RelationGetExclusionInfo -- get info about index's exclusion constraint
  *
- * This should be called only for an index that is known to have an
- * associated exclusion constraint.  It returns arrays (palloc'd in caller's
- * context) of the exclusion operator OIDs, their underlying functions'
- * OIDs, and their strategy numbers in the index's opclasses.  We cache
- * all this information since it requires a fair amount of work to get.
+ * This should be called only for an index that is known to have an associated
+ * exclusion constraint or primary key/unique constraint using WITHOUT
+ * OVERLAPS.
+
+ * It returns arrays (palloc'd in caller's context) of the exclusion operator
+ * OIDs, their underlying functions' OIDs, and their strategy numbers in the
+ * index's opclasses.  We cache all this information since it requires a fair
+ * amount of work to get.
  */
 void
 RelationGetExclusionInfo(Relation indexRelation,
@@ -5636,7 +5639,10 @@ RelationGetExclusionInfo(Relation indexRelation,
 		int			nelem;
 
 		/* We want the exclusion constraint owning the index */
-		if (conform->contype != CONSTRAINT_EXCLUSION ||
+		if ((conform->contype != CONSTRAINT_EXCLUSION &&
+					!(conform->conwithoutoverlaps && (
+							conform->contype == CONSTRAINT_PRIMARY
+							|| conform->contype == CONSTRAINT_UNIQUE))) ||
 			conform->conindid != RelationGetRelid(indexRelation))
 			continue;
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce4..a19443becd6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7233,6 +7233,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conwithoutoverlaps,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7314,10 +7315,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	if (fout->remoteVersion >= 150000)
 		appendPQExpBufferStr(query,
-							 "i.indnullsnotdistinct ");
+							 "i.indnullsnotdistinct, ");
 	else
 		appendPQExpBufferStr(query,
-							 "false AS indnullsnotdistinct ");
+							 "false AS indnullsnotdistinct, ");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "c.conwithoutoverlaps ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS conwithoutoverlaps ");
 
 	/*
 	 * The point of the messy-looking outer join is to find a constraint that
@@ -7385,6 +7393,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_conname = PQfnumber(res, "conname");
 	i_condeferrable = PQfnumber(res, "condeferrable");
 	i_condeferred = PQfnumber(res, "condeferred");
+	i_conwithoutoverlaps = PQfnumber(res, "conwithoutoverlaps");
 	i_contableoid = PQfnumber(res, "contableoid");
 	i_conoid = PQfnumber(res, "conoid");
 	i_condef = PQfnumber(res, "condef");
@@ -7492,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->conindex = indxinfo[j].dobj.dumpId;
 				constrinfo->condeferrable = *(PQgetvalue(res, j, i_condeferrable)) == 't';
 				constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
+				constrinfo->conwithoutoverlaps = *(PQgetvalue(res, j, i_conwithoutoverlaps)) == 't';
 				constrinfo->conislocal = true;
 				constrinfo->separate = true;
 
@@ -17129,6 +17139,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conwithoutoverlaps)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
 			if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
 				appendPQExpBufferStr(q, ") INCLUDE (");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f0772d21579..f523c3a590d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -479,6 +479,7 @@ typedef struct _constraintInfo
 	DumpId		conindex;		/* identifies associated index if any */
 	bool		condeferrable;	/* true if constraint is DEFERRABLE */
 	bool		condeferred;	/* true if constraint is INITIALLY DEFERRED */
+	bool		conwithoutoverlaps;	/* true if the constraint is WITHOUT OVERLAPS */
 	bool		conislocal;		/* true if constraint has local definition */
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 3912dbf481a..4a4d81c4d9f 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1015,6 +1015,46 @@ my %tests = (
 		},
 	},
 
+	'CONSTRAINT PRIMARY KEY / WITHOUT OVERLAPS' => {
+		create_sql  => 'CREATE TABLE dump_test.test_table_tpk (
+							col1 int4range,
+							col2 tstzrange,
+							CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS));',
+		regexp => qr/^
+			\QALTER TABLE ONLY dump_test.test_table_tpk\E \n^\s+
+			\QADD CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS);\E
+			/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_post_data  => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'CONSTRAINT UNIQUE / WITHOUT OVERLAPS' => {
+		create_sql  => 'CREATE TABLE dump_test.test_table_tuq (
+							col1 int4range,
+							col2 tstzrange,
+							CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS));',
+		regexp => qr/^
+			\QALTER TABLE ONLY dump_test.test_table_tuq\E \n^\s+
+			\QADD CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS);\E
+			/xm,
+		like => {
+			%full_runs,
+			%dump_test_schema_runs,
+			section_post_data  => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
 	'ALTER TABLE (partitioned) ADD CONSTRAINT ... FOREIGN KEY' => {
 		create_order => 4,
 		create_sql => 'CREATE TABLE dump_test.test_table_fk (
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 37f95163201..3f8bff59548 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2379,6 +2379,10 @@ describeOneTableDetails(const char *schemaname,
 			else
 				appendPQExpBufferStr(&buf, ", false AS indisreplident");
 			appendPQExpBufferStr(&buf, ", c2.reltablespace");
+			if (pset.sversion >= 170000)
+				appendPQExpBufferStr(&buf, ", con.conwithoutoverlaps");
+			else
+				appendPQExpBufferStr(&buf, ", false AS conwithoutoverlaps");
 			appendPQExpBuffer(&buf,
 							  "\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n"
 							  "  LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\n"
@@ -2400,8 +2404,12 @@ describeOneTableDetails(const char *schemaname,
 					printfPQExpBuffer(&buf, "    \"%s\"",
 									  PQgetvalue(result, i, 0));
 
-					/* If exclusion constraint, print the constraintdef */
-					if (strcmp(PQgetvalue(result, i, 7), "x") == 0)
+					/*
+					 * If exclusion constraint or PK/UNIQUE constraint WITHOUT
+					 * OVERLAPS, print the constraintdef
+					 */
+					if (strcmp(PQgetvalue(result, i, 7), "x") == 0 ||
+							strcmp(PQgetvalue(result, i, 12), "t") == 0)
 					{
 						appendPQExpBuffer(&buf, " %s",
 										  PQgetvalue(result, i, 6));
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index e7ced18a5ba..22dd04c1418 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -17,6 +17,7 @@
 #define GIST_H
 
 #include "access/itup.h"
+#include "access/stratnum.h"
 #include "access/transam.h"
 #include "access/xlog.h"
 #include "access/xlogdefs.h"
@@ -246,4 +247,6 @@ typedef struct
 	do { (e).key = (k); (e).rel = (r); (e).page = (pg); \
 		 (e).offset = (o); (e).leafkey = (l); } while (0)
 
+extern StrategyNumber GistTranslateStratnum(Oid opclass, StrategyNumber strat);
+
 #endif							/* GIST_H */
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 99dab5940bc..2ef8512dbff 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -92,6 +92,7 @@ extern Oid	index_create(Relation heapRelation,
 #define	INDEX_CONSTR_CREATE_INIT_DEFERRED	(1 << 2)
 #define	INDEX_CONSTR_CREATE_UPDATE_INDEX	(1 << 3)
 #define	INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS	(1 << 4)
+#define	INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS (1 << 5)
 
 extern Oid	index_concurrently_create_copy(Relation heapRelation,
 										   Oid oldIndexId,
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index d79432e142b..8fd76a94d78 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -107,6 +107,12 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	/* Has a local definition and cannot be inherited */
 	bool		connoinherit;
 
+	/*
+	 * For primary keys and unique constraints, signifies the last column uses
+	 * overlaps instead of equals.
+	 */
+	bool		conwithoutoverlaps;
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -146,7 +152,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 
 	/*
 	 * If an exclusion constraint, the OIDs of the exclusion operators for
-	 * each column of the constraint
+	 * each column of the constraint.  Also set for unique constraints/primary
+	 * keys using WITHOUT OVERLAPS.
 	 */
 	Oid			conexclop[1] BKI_LOOKUP(pg_operator);
 
@@ -236,6 +243,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
+								  bool conWithoutOverlaps,
 								  bool is_internal);
 
 extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 29c511e3196..4352a438899 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -14,6 +14,7 @@
 #ifndef DEFREM_H
 #define DEFREM_H
 
+#include "access/stratnum.h"
 #include "catalog/objectaddress.h"
 #include "nodes/params.h"
 #include "parser/parse_node.h"
@@ -44,10 +45,14 @@ extern char *ChooseRelationName(const char *name1, const char *name2,
 extern bool CheckIndexCompatible(Oid oldId,
 								 const char *accessMethodName,
 								 const List *attributeList,
-								 const List *exclusionOpNames);
+								 const List *exclusionOpNames,
+								 bool isWithoutOverlaps);
 extern Oid	GetDefaultOpClass(Oid type_id, Oid am_id);
 extern Oid	ResolveOpClass(const List *opclass, Oid attrType,
 						   const char *accessMethodName, Oid accessMethodId);
+extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
+											 const char *opname, Oid *opid,
+											 StrategyNumber *strat);
 
 /* commands/functioncmds.c */
 extern ObjectAddress CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b3181f34aee..476d55dd240 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2590,6 +2590,7 @@ typedef struct Constraint
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
 	List	   *keys;			/* String nodes naming referenced key
 								 * column(s); also used for NOT NULL */
+	bool		without_overlaps;	/* WITHOUT OVERLAPS specified */
 	List	   *including;		/* String nodes naming referenced nonkey
 								 * column(s) */
 
@@ -3219,6 +3220,7 @@ typedef struct IndexStmt
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
 	bool		primary;		/* is index a primary key? */
 	bool		isconstraint;	/* is it for a pkey/unique constraint? */
+	bool		iswithoutoverlaps;	/* is the constraint WITHOUT OVERLAPS? */
 	bool		deferrable;		/* is the constraint DEFERRABLE? */
 	bool		initdeferred;	/* is the constraint INITIALLY DEFERRED? */
 	bool		transformed;	/* true when transformIndexStmt is finished */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644
index 00000000000..726e94102bf
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,395 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+--
+-- test input parser
+--
+-- PK with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+ERROR:  constraint using WITHOUT OVERLAPS needs at least two columns
+-- PK with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng (
+	id INTEGER,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" named in key does not exist
+LINE 3:  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU...
+         ^
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  data type text has no default operator class for access method "gist"
+HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+-- PK with one column plus a range:
+CREATE TABLE temporal_rng (
+	-- Since we can't depend on having btree_gist here,
+	-- use an int4range instead of an int.
+	-- (The rangetypes regression test uses the same trick.)
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+              Table "public.temporal_rng"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | tsrange   |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+            pg_get_constraintdef             
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+                                pg_get_indexdef                                
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
+(1 row)
+
+-- PK with two columns plus a range:
+-- We don't drop this table because tests below also need multiple scalar columns.
+CREATE TABLE temporal_rng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id1      | int4range |           | not null | 
+ id2      | int4range |           | not null | 
+ valid_at | tsrange   |           | not null | 
+Indexes:
+    "temporal_rng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+               pg_get_constraintdef                
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at)
+(1 row)
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+-- PK with a multirange:
+CREATE TABLE temporal_mltrng (
+  id int4range,
+  valid_at tsmultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng
+              Table "public.temporal_mltrng"
+  Column  |     Type     | Collation | Nullable | Default 
+----------+--------------+-----------+----------+---------
+ id       | int4range    |           | not null | 
+ valid_at | tsmultirange |           | not null | 
+Indexes:
+    "temporal_mltrng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng3 (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR:  constraint using WITHOUT OVERLAPS needs at least two columns
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng3 (
+	id INTEGER,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" named in key does not exist
+LINE 3:  CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV...
+         ^
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  data type text has no default operator class for access method "gist"
+HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+             Table "public.temporal_rng3"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           |          | 
+ valid_at | tsrange   |           |          | 
+Indexes:
+    "temporal_rng3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+          pg_get_constraintdef          
+----------------------------------------
+ UNIQUE (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+                                 pg_get_indexdef                                 
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+	id1 int4range,
+	id2 int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+             Table "public.temporal_rng3"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id1      | int4range |           |          | 
+ id2      | int4range |           |          | 
+ valid_at | tsrange   |           |          | 
+Indexes:
+    "temporal_rng3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+             pg_get_constraintdef             
+----------------------------------------------
+ UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+-- UNIQUE with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at tsrange
+);
+ALTER TABLE temporal_rng
+	ADD CONSTRAINT temporal_rng_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY USING INDEX idx_temporal3_uq;
+ERROR:  "idx_temporal3_uq" is not a unique index
+LINE 2:  ADD CONSTRAINT temporal3_pk
+             ^
+DETAIL:  Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+ERROR:  "idx_temporal3_uq" is not a unique index
+LINE 2:  ADD CONSTRAINT temporal3_uq
+             ^
+DETAIL:  Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+NOTICE:  ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "idx_temporal3_uq" to "temporal3_uq"
+DROP TABLE temporal3;
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at tsrange,
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at tsrange,
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+--
+-- test PK inserts
+--
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")) conflicts with existing key (id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+ERROR:  null value in column "id" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")).
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains ([3,4), null).
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_at daterange,
+  id2 int8range,
+  name TEXT,
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+  ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+--
+-- test changing the PK's dependencies
+--
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ERROR:  column "valid_at" is in a primary key
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+--
+-- test PARTITION BY for ranges
+--
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+ [3,4) | [01-01-2000,01-01-2010) | three
+(3 rows)
+
+SELECT * FROM tp1 ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+(2 rows)
+
+SELECT * FROM tp2 ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [3,4) | [01-01-2000,01-01-2010) | three
+(1 row)
+
+DROP TABLE temporal_partitioned;
+-- temporal UNIQUE:
+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 tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three');
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+ [3,4) | [01-01-2000,01-01-2010) | three
+(3 rows)
+
+SELECT * FROM tp1 ORDER BY id, valid_at;
+  id   |        valid_at         | name 
+-------+-------------------------+------
+ [1,2) | [01-01-2000,02-01-2000) | one
+ [1,2) | [02-01-2000,03-01-2000) | one
+(2 rows)
+
+SELECT * FROM tp2 ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [3,4) | [01-01-2000,01-01-2010) | three
+(1 row)
+
+DROP TABLE temporal_partitioned;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537b..b08aaae44b8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin_bloom brin_multi
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
 # ----------
-test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
+test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role without_overlaps
 
 # collate.*.utf8 tests cannot be run in parallel with each other
 test: rules psql psql_crosstab amutils stats_ext collate.linux.utf8 collate.windows.win1252
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 00000000000..c8e8ab994a0
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,290 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+
+--
+-- test input parser
+--
+
+-- PK with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng (
+	id INTEGER,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a non-range column:
+
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with one column plus a range:
+
+CREATE TABLE temporal_rng (
+	-- Since we can't depend on having btree_gist here,
+	-- use an int4range instead of an int.
+	-- (The rangetypes regression test uses the same trick.)
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+
+-- PK with two columns plus a range:
+-- We don't drop this table because tests below also need multiple scalar columns.
+CREATE TABLE temporal_rng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+
+-- PK with a multirange:
+CREATE TABLE temporal_mltrng (
+  id int4range,
+  valid_at tsmultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng3 (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng3 (
+	id INTEGER,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+	id1 int4range,
+	id2 int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+-- UNIQUE with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at tsrange
+);
+ALTER TABLE temporal_rng
+	ADD CONSTRAINT temporal_rng_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange
+);
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at tsrange,
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at tsrange,
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+--
+-- test PK inserts
+--
+
+-- okay:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL));
+
+-- should fail:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
+
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_at daterange,
+  id2 int8range,
+  name TEXT,
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
+  ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar')
+;
+DROP TABLE temporal3;
+
+--
+-- test changing the PK's dependencies
+--
+
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+
+--
+-- test PARTITION BY for ranges
+--
+
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[3,3]', 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;
+DROP TABLE temporal_partitioned;
+
+-- temporal UNIQUE:
+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 tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[3,3]', 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;
+DROP TABLE temporal_partitioned;
-- 
2.42.0

From b173b6aa92a7b20260c8040572e2b8f3d38665fa Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 29 Dec 2023 14:36:27 -0800
Subject: [PATCH v23 3/8] Add GiST referencedagg support func

This is the thirteenth support function and lets us implement temporal
foreign keys without hardcoding the range_agg function. The support
function should be an aggregate function that takes a type matching the
foreign key's PERIOD element and returns a type that can appear on the
righthand side of a ContainedBy operator, so that for example we can say
`fkperiod <@ range_agg(pkperiod)`.
---
 doc/src/sgml/gist.sgml                 | 112 +++++++++++++++++++++++++
 doc/src/sgml/xindex.sgml               |   8 +-
 src/backend/access/gist/gistvalidate.c |   7 +-
 src/backend/access/index/amvalidate.c  |   8 +-
 src/include/access/gist.h              |   3 +-
 src/include/catalog/pg_amproc.dat      |   6 ++
 6 files changed, 138 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 86be7c96256..87948a77cfd 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -300,6 +300,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
    used by the opclass. This lets the core code look up operators for temporal
    constraint indexes.
+   The optional thirteenth method <function>referencedagg</function> is used by
+   temporal foreign keys to combined referenced rows with the same
+   non-<literal>WITHOUT OVERLAPS</literal> into one <literal>WITHOUT OVERLAPS</literal>
+   span to compare against referencing rows.
  </para>
 
  <variablelist>
@@ -1248,6 +1252,114 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><function>referencedagg</function></term>
+     <listitem>
+      <para>
+       An aggregate function. Given values of this opclass,
+       it returns a value combining them all. The return value
+       need not be the same type as the input, but it must be a
+       type that can appear on the right hand side of the "contained by"
+       operator. For example the built-in <literal>range_ops</literal>
+       opclass uses <literal>range_agg</literal> here, so that foreign
+       keys can check <literal>fkperiod @> range_agg(pkperiod)</literal>.
+      </para>
+      <para>
+       This is used for temporal foreign key constraints.
+       If you omit this support function, your type cannot be used
+       as the <literal>PERIOD</literal> part of a foreign key.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this (using <literal>my_range_agg</literal> as an example):
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_range_agg_transfn(internal, anyrange)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION my_range_agg_finalfn(internal, anyrange)
+RETURNS anymultirange
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE AGGREGATE my_range_agg(anyrange) {
+  SFUNC = my_range_agg_transfn,
+  STYPE = internal,
+  FINALFUNC = my_range_agg_finalfn
+};
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this example:
+
+<programlisting>
+Datum
+my_range_agg_transfn(PG_FUNCTION_ARGS)
+{
+    MemoryContext    aggContext;
+    Oid              rngtypoid;
+    ArrayBuildState *state;
+
+    if (!AggCheckCallContext(fcinfo, &amp;aggContext))
+        elog(ERROR, "range_agg_transfn called in non-aggregate context");
+
+    rngtypoid = get_fn_expr_argtype(fcinfo-&gt;flinfo, 1);
+    if (!type_is_range(rngtypoid))
+        elog(ERROR, "range_agg must be called with a range");
+
+    if (PG_ARGISNULL(0))
+        state = initArrayResult(rngtypoid, aggContext, false);
+    else
+        state = (ArrayBuildState *) PG_GETARG_POINTER(0);
+
+    /* skip NULLs */
+    if (!PG_ARGISNULL(1))
+        accumArrayResult(state, PG_GETARG_DATUM(1), false, rngtypoid, aggContext);
+
+    PG_RETURN_POINTER(state);
+}
+
+Datum
+my_range_agg_finalfn(PG_FUNCTION_ARGS)
+{
+    MemoryContext    aggContext;
+    Oid              mltrngtypoid;
+    TypeCacheEntry  *typcache;
+    ArrayBuildState *state;
+    int32            range_count;
+    RangeType      **ranges;
+    int              i;
+
+    if (!AggCheckCallContext(fcinfo, &amp;aggContext))
+        elog(ERROR, "range_agg_finalfn called in non-aggregate context");
+
+    state = PG_ARGISNULL(0) ? NULL : (ArrayBuildState *) PG_GETARG_POINTER(0);
+    if (state == NULL)
+        /* This shouldn't be possible, but just in case.... */
+        PG_RETURN_NULL();
+
+    /* Also return NULL if we had zero inputs, like other aggregates */
+    range_count = state-&gt;nelems;
+    if (range_count == 0)
+        PG_RETURN_NULL();
+
+    mltrngtypoid = get_fn_expr_rettype(fcinfo-&gt;flinfo);
+    typcache = multirange_get_typcache(fcinfo, mltrngtypoid);
+
+    ranges = palloc0(range_count * sizeof(RangeType *));
+    for (i = 0; i &lt; range_count; i++)
+        ranges[i] = DatumGetRangeTypeP(state-&gt;dvalues[i]);
+
+    PG_RETURN_MULTIRANGE_P(make_multirange(mltrngtypoid, typcache-&gt;rngtype, range_count, ranges));
+}
+</programlisting>
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 8c4ede31057..3ee13617171 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
-   GiST indexes have twelve support functions, seven of which are optional,
+   GiST indexes have thirteen support functions, eight of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -596,6 +596,12 @@
         used by the opclass (optional)</entry>
        <entry>12</entry>
       </row>
+      <row>
+       <entry><function>referencedagg</function></entry>
+       <entry>aggregates referenced rows' <literal>WITHOUT OVERLAPS</literal>
+        part</entry>
+       <entry>13</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 698e01ed2f7..4bd2e531d71 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -151,6 +151,11 @@ gistvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, INT2OID, true,
 											1, 1, INT2OID);
 				break;
+			case GIST_REFERENCED_AGG_PROC:
+				ok = check_amproc_signature(procform->amproc, InvalidOid, false,
+											   1, 1, opcintype);
+				// TODO: must be aggregate
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -272,7 +277,7 @@ gistvalidate(Oid opclassoid)
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
 			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
-			i == GIST_STRATNUM_PROC)
+			i == GIST_STRATNUM_PROC || i == GIST_REFERENCED_AGG_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/access/index/amvalidate.c b/src/backend/access/index/amvalidate.c
index 32bb477f328..71638cb401b 100644
--- a/src/backend/access/index/amvalidate.c
+++ b/src/backend/access/index/amvalidate.c
@@ -146,7 +146,8 @@ identify_opfamily_groups(CatCList *oprlist, CatCList *proclist)
  *
  * The "..." represents maxargs argument-type OIDs.  If "exact" is true, they
  * must match the function arg types exactly, else only binary-coercibly.
- * In any case the function result type must match restype exactly.
+ * In any case the function result type must match restype exactly,
+ * unless it is InvalidOid.
  */
 bool
 check_amproc_signature(Oid funcid, Oid restype, bool exact,
@@ -163,8 +164,9 @@ check_amproc_signature(Oid funcid, Oid restype, bool exact,
 		elog(ERROR, "cache lookup failed for function %u", funcid);
 	procform = (Form_pg_proc) GETSTRUCT(tp);
 
-	if (procform->prorettype != restype || procform->proretset ||
-		procform->pronargs < minargs || procform->pronargs > maxargs)
+	if ((procform->prorettype != restype && OidIsValid(restype))
+		|| procform->proretset || procform->pronargs < minargs
+		|| procform->pronargs > maxargs)
 		result = false;
 
 	va_start(ap, maxargs);
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index 22dd04c1418..641677e191c 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -40,7 +40,8 @@
 #define GIST_OPTIONS_PROC				10
 #define GIST_SORTSUPPORT_PROC			11
 #define GIST_STRATNUM_PROC				12
-#define GISTNProcs					12
+#define GIST_REFERENCED_AGG_PROC		13
+#define GISTNProcs					13
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 352558c1f06..1d3d5fcf4d8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -610,6 +610,9 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '13',
+  amproc => 'range_agg(anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -650,6 +653,9 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '13',
+  amproc => 'range_agg(anymultirange)' },
 
 # gin
 { amprocfamily => 'gin/array_ops', amproclefttype => 'anyarray',
-- 
2.42.0

From 96f4cae29b169fdf8f342dcf6befd83aa59f0ca8 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v23 4/8] Add temporal FOREIGN KEYs

- Added bison support for temporal FOREIGN KEYs. They should have a
  range column for the temporal component.
- Added temporal trigger functions akin to existing RI trigger
  functions.
- Don't yet support ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}.
- Added pg_dump support.
- Show the correct syntax in psql \d output for foreign keys.
- Added tests and documentation.
---
 .../btree_gist/expected/without_overlaps.out  |  48 +
 contrib/btree_gist/sql/without_overlaps.sql   |  28 +
 doc/src/sgml/ref/create_table.sgml            |  48 +-
 src/backend/commands/indexcmds.c              |  24 +-
 src/backend/commands/tablecmds.c              | 916 ++++++++++++------
 src/backend/nodes/outfuncs.c                  |   2 +
 src/backend/nodes/readfuncs.c                 |   2 +
 src/backend/parser/gram.y                     |  37 +-
 src/backend/utils/adt/ri_triggers.c           | 404 +++++++-
 src/backend/utils/adt/ruleutils.c             |  19 +-
 src/backend/utils/cache/lsyscache.c           |  31 +
 src/include/catalog/pg_constraint.h           |   8 +-
 src/include/catalog/pg_proc.dat               |  24 +
 src/include/commands/defrem.h                 |   6 +-
 src/include/commands/tablecmds.h              |   3 +
 src/include/nodes/parsenodes.h                |   6 +
 src/include/parser/kwlist.h                   |   1 +
 src/include/utils/lsyscache.h                 |   1 +
 .../regress/expected/without_overlaps.out     | 561 +++++++++++
 src/test/regress/sql/without_overlaps.sql     | 535 ++++++++++
 20 files changed, 2329 insertions(+), 375 deletions(-)

diff --git a/contrib/btree_gist/expected/without_overlaps.out b/contrib/btree_gist/expected/without_overlaps.out
index be52c522e89..6d4aca1d6d5 100644
--- a/contrib/btree_gist/expected/without_overlaps.out
+++ b/contrib/btree_gist/expected/without_overlaps.out
@@ -42,3 +42,51 @@ INSERT INTO temporal_rng VALUES
   (1, '[2000-06-01,2001-01-01)');
 ERROR:  conflicting key value violates exclusion constraint "temporal_rng_pk"
 DETAIL:  Key (id, valid_at)=(1, [06-01-2000,01-01-2001)) conflicts with existing key (id, valid_at)=(1, [01-01-2000,01-01-2001)).
+-- Foreign key
+CREATE TABLE temporal_fk_rng2rng (
+  id integer,
+  valid_at daterange,
+  parent_id integer,
+  CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+\d temporal_fk_rng2rng
+           Table "public.temporal_fk_rng2rng"
+  Column   |   Type    | Collation | Nullable | Default 
+-----------+-----------+-----------+----------+---------
+ id        | integer   |           | not null | 
+ valid_at  | daterange |           | not null | 
+ parent_id | integer   |           |          | 
+Indexes:
+    "temporal_fk_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk_rng2rng_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+-- okay
+INSERT INTO temporal_fk_rng2rng VALUES
+  (1, '[2000-01-01,2001-01-01)', 1);
+-- okay spanning two parent records:
+INSERT INTO temporal_fk_rng2rng VALUES
+  (2, '[2000-01-01,2002-01-01)', 1);
+-- key is missing
+INSERT INTO temporal_fk_rng2rng VALUES
+  (3, '[2000-01-01,2001-01-01)', 3);
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=(3, [01-01-2000,01-01-2001)) is not present in table "temporal_rng".
+-- key exusts but is outside range
+INSERT INTO temporal_fk_rng2rng VALUES
+  (4, '[2001-01-01,2002-01-01)', 2);
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=(2, [01-01-2001,01-01-2002)) is not present in table "temporal_rng".
+-- key exusts but is partly outside range
+INSERT INTO temporal_fk_rng2rng VALUES
+  (5, '[2000-01-01,2002-01-01)', 2);
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=(2, [01-01-2000,01-01-2002)) is not present in table "temporal_rng".
diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql
index 433c609ab22..0484135e766 100644
--- a/contrib/btree_gist/sql/without_overlaps.sql
+++ b/contrib/btree_gist/sql/without_overlaps.sql
@@ -23,3 +23,31 @@ INSERT INTO temporal_rng VALUES
 -- should fail:
 INSERT INTO temporal_rng VALUES
   (1, '[2000-06-01,2001-01-01)');
+
+-- Foreign key
+CREATE TABLE temporal_fk_rng2rng (
+  id integer,
+  valid_at daterange,
+  parent_id integer,
+  CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+\d temporal_fk_rng2rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+
+-- okay
+INSERT INTO temporal_fk_rng2rng VALUES
+  (1, '[2000-01-01,2001-01-01)', 1);
+-- okay spanning two parent records:
+INSERT INTO temporal_fk_rng2rng VALUES
+  (2, '[2000-01-01,2002-01-01)', 1);
+-- key is missing
+INSERT INTO temporal_fk_rng2rng VALUES
+  (3, '[2000-01-01,2001-01-01)', 3);
+-- key exusts but is outside range
+INSERT INTO temporal_fk_rng2rng VALUES
+  (4, '[2001-01-01,2002-01-01)', 2);
+-- key exusts but is partly outside range
+INSERT INTO temporal_fk_rng2rng VALUES
+  (5, '[2000-01-01,2002-01-01)', 2);
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 278af2e3869..69081759d43 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -81,7 +81,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
   UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
 class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -1150,8 +1150,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    <varlistentry id="sql-createtable-parms-references">
     <term><literal>REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH <replaceable class="parameter">matchtype</replaceable> ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal> (column constraint)</term>
 
-   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
-    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] )
+    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">temporal_interval</replaceable> ] ) ]
     [ MATCH <replaceable class="parameter">matchtype</replaceable> ]
     [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
     [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1162,11 +1162,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       These clauses specify a foreign key constraint, which requires
       that a group of one or more columns of the new table must only
       contain values that match values in the referenced
-      column(s) of some row of the referenced table.  If the <replaceable
+      column(s) of some row of the referenced table.
+      If the <replaceable
       class="parameter">refcolumn</replaceable> list is omitted, the
       primary key of the <replaceable class="parameter">reftable</replaceable>
       is used.  The referenced columns must be the columns of a non-deferrable
-      unique or primary key constraint in the referenced table.  The user
+      unique or primary key constraint in the referenced table.
+     </para>
+
+     <para>
+      If the last column is marked with <literal>PERIOD</literal>,
+      it must be a period or range column, and the referenced table
+      must have a temporal primary key.
+      The non-<literal>PERIOD</literal> columns are treated normally
+      (and there must be at least one of them),
+      but the <literal>PERIOD</literal> column is not compared for equality.
+      Instead the constraint is considered satisfied
+      if the referenced table has matching records whose combined ranges completely cover
+      the referencing record.
+      In other words, the reference must have a referent for its entire duration.
+     </para>
+
+     <para>
+      The user
       must have <literal>REFERENCES</literal> permission on the referenced table
       (either the whole table, or the specific referenced columns).  The
       addition of a foreign key constraint requires a
@@ -1240,6 +1258,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           values of the referencing column(s) to the new values of the
           referenced columns, respectively.
          </para>
+
+         <para>
+          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>
 
@@ -1251,6 +1275,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           referencing columns, to null. A subset of columns can only be
           specified for <literal>ON DELETE</literal> actions.
          </para>
+
+         <para>
+          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>
 
@@ -1264,6 +1295,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           (There must be a row in the referenced table matching the default
           values, if they are not null, or the operation will fail.)
          </para>
+
+         <para>
+          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>
       </variablelist>
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 52f3194791d..57bf7e03744 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2189,8 +2189,9 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				opname = "equals";
 			}
 			GetOperatorFromWellKnownStrategy(opclassOids[attn],
-											 atttype,
+											 InvalidOid,
 											 opname,
+											 "WITHOUT OVERLAPS constraint",
 											 &opid,
 											 &strat);
 			indexInfo->ii_ExclusionOps[attn] = opid;
@@ -2431,8 +2432,9 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * GetOperatorFromWellKnownStrategy
  *
  * opclass - the opclass to use
- * atttype - the type to ask about
+ * rhstype - the type for the right-hand side
  * opname - used to build error messages
+ * context - used to build error messages
  * opid - holds the operator we found
  * strat - holds the input and output strategy number
  *
@@ -2448,8 +2450,9 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  */
 void
 GetOperatorFromWellKnownStrategy(Oid opclass,
-								 Oid atttype,
+								 Oid rhstype,
 								 const char *opname,
+								 const char *context,
 								 Oid *opid,
 								 StrategyNumber *strat)
 {
@@ -2473,12 +2476,19 @@ GetOperatorFromWellKnownStrategy(Oid opclass,
 		if (*strat == InvalidStrategy)
 			ereport(ERROR,
 					(errcode(ERRCODE_UNDEFINED_OBJECT),
-					 errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+					 errmsg("no %s operator found for %s", opname, context),
 					 errdetail("Could not translate strategy number %u for opclass %d.",
 						 opstrat, opclass),
 					 errhint("Define a stratnum support function for your GiST opclass.")));
 
-		*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
+		/*
+		 * We parameterize rhstype so foreign keys can ask for a <@ operator
+		 * whose rhs matches the aggregate function. For example range_agg
+		 * returns anymultirange.
+		 */
+		if (!OidIsValid(rhstype))
+			rhstype = opcintype;
+		*opid = get_opfamily_member(opfamily, opcintype, rhstype, *strat);
 	}
 
 	if (!OidIsValid(*opid))
@@ -2500,11 +2510,11 @@ GetOperatorFromWellKnownStrategy(Oid opclass,
 
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_OBJECT),
-				 errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+				 errmsg("no %s operator found for %s", opname, context),
 				 errdetail("There must be an %s operator within opfamily \"%s\" for type \"%s\".",
 						   opname,
 						   NameStr(opfform->opfname),
-						   format_type_be(atttype))));
+						   format_type_be(opcintype))));
 	}
 }
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e8622b7a8e5..cdef174ee24 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@
 
 #include "access/attmap.h"
 #include "access/genam.h"
+#include "access/gist.h"
 #include "access/heapam.h"
 #include "access/heapam_xlog.h"
 #include "access/multixact.h"
@@ -212,8 +213,11 @@ typedef struct NewConstraint
 	ConstrType	contype;		/* CHECK or FOREIGN */
 	Oid			refrelid;		/* PK rel, if FOREIGN */
 	Oid			refindid;		/* OID of PK's index, if FOREIGN */
+	bool		conwithperiod;	/* Whether the new FOREIGN KEY uses PERIOD */
 	Oid			conid;			/* OID of pg_constraint entry, if FOREIGN */
 	Node	   *qual;			/* Check expr or CONSTR_FOREIGN Constraint */
+	Oid		   *operoids;		/* oper oids for FOREIGN KEY with PERIOD */
+	Oid		   *procoids;		/* proc oids for FOREIGN KEY with PERIOD */
 	ExprState  *qualstate;		/* Execution state for CHECK expr */
 } NewConstraint;
 
@@ -384,17 +388,20 @@ static int	transformColumnNameList(Oid relId, List *colList,
 									int16 *attnums, Oid *atttypids);
 static int	transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 									   List **attnamelist,
+									   Node **pk_period,
 									   int16 *attnums, Oid *atttypids,
+									   int16 *periodattnums, Oid *periodatttypids,
 									   Oid *opclasses);
 static Oid	transformFkeyCheckAttrs(Relation pkrel,
 									int numattrs, int16 *attnums,
+									bool is_temporal, int16 periodattnum,
 									Oid *opclasses);
 static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
 static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
 									 Oid *funcid);
 static void validateForeignKeyConstraint(char *conname,
 										 Relation rel, Relation pkrel,
-										 Oid pkindOid, Oid constraintOid);
+										 Oid pkindOid, Oid constraintOid, bool temporal);
 static void ATController(AlterTableStmt *parsetree,
 						 Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
 						 AlterTableUtilityContext *context);
@@ -507,7 +514,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
 											Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 											int numfkdelsetcols, int16 *fkdelsetcols,
 											bool old_check_ok,
-											Oid parentDelTrigger, Oid parentUpdTrigger);
+											Oid parentDelTrigger, Oid parentUpdTrigger,
+											bool is_temporal);
 static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
@@ -517,7 +525,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
 									Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 									int numfkdelsetcols, int16 *fkdelsetcols,
 									bool old_check_ok, LOCKMODE lockmode,
-									Oid parentInsTrigger, Oid parentUpdTrigger);
+									Oid parentInsTrigger, Oid parentUpdTrigger,
+									bool is_temporal);
+
 static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
 									   Relation partitionRel);
 static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -549,6 +559,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
 									   Oid conoid, Oid confrelid, Oid conrelid,
 									   Oid *insertTriggerOid,
 									   Oid *updateTriggerOid);
+static void FindFKComparisonOperators(Constraint *fkconstraint,
+					AlteredTableInfo *tab, int i, int16 *fkattnum,
+					bool *old_check_ok, ListCell **old_pfeqop_item,
+					Oid pktype, Oid fktype, Oid opclass,
+					bool is_temporal, bool for_overlaps,
+					Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut);
 static void ATExecDropConstraint(Relation rel, const char *constrName,
 								 DropBehavior behavior, bool recurse,
 								 bool missing_ok, LOCKMODE lockmode);
@@ -5931,7 +5947,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 
 				validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
 											 con->refindid,
-											 con->conid);
+											 con->conid,
+											 con->conwithperiod);
 
 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9770,6 +9787,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	Oid			ppeqoperators[INDEX_MAX_KEYS] = {0};
 	Oid			ffeqoperators[INDEX_MAX_KEYS] = {0};
 	int16		fkdelsetcols[INDEX_MAX_KEYS] = {0};
+	bool		is_temporal;
+	int16		pkperiodattnum = 0;
+	int16		fkperiodattnum = 0;
+	Oid			pkperiodtypoid = 0;
+	Oid			fkperiodtypoid = 0;
+	Oid			periodoperoid;
+	Oid			periodprocoid;
 	int			i;
 	int			numfks,
 				numpks,
@@ -9864,6 +9888,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfks = transformColumnNameList(RelationGetRelid(rel),
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
+	is_temporal = (fkconstraint->pk_period || fkconstraint->fk_period);
+	if (is_temporal)
+	{
+		if (!fkconstraint->fk_period)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FOREIGN_KEY),
+					errmsg("foreign key uses PERIOD on the referenced table but not the referencing table")));
+		transformColumnNameList(RelationGetRelid(rel),
+							  list_make1(fkconstraint->fk_period),
+							  &fkperiodattnum, &fkperiodtypoid);
+	}
 
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
@@ -9882,16 +9917,38 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
 											&fkconstraint->pk_attrs,
+											&fkconstraint->pk_period,
 											pkattnum, pktypoid,
+											&pkperiodattnum, &pkperiodtypoid,
 											opclasses);
+
+		/* If the primary key uses WITHOUT OVERLAPS, the fk must use PERIOD */
+		if (pkperiodattnum && !fkperiodattnum)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FOREIGN_KEY),
+					errmsg("foreign key uses PERIOD on the referenced table but not the referencing table")));
 	}
 	else
 	{
 		numpks = transformColumnNameList(RelationGetRelid(pkrel),
 										 fkconstraint->pk_attrs,
 										 pkattnum, pktypoid);
+		if (is_temporal)
+		{
+			/* Since we got pk_attrs, we should have pk_period too. */
+			if (!fkconstraint->pk_period)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_FOREIGN_KEY),
+						errmsg("foreign key uses PERIOD on the referencing table but not the referenced table")));
+
+			transformColumnNameList(RelationGetRelid(pkrel),
+									list_make1(fkconstraint->pk_period),
+									&pkperiodattnum, &pkperiodtypoid);
+		}
+
 		/* Look for an index matching the column list */
 		indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+										   is_temporal, pkperiodattnum,
 										   opclasses);
 	}
 
@@ -9900,34 +9957,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	checkFkeyPermissions(pkrel, pkattnum, numpks);
 
-	/*
-	 * Check some things for generated columns.
-	 */
-	for (i = 0; i < numfks; i++)
-	{
-		char		attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;
-
-		if (attgenerated)
-		{
-			/*
-			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
-			 */
-			if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
-				fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT ||
-				fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("invalid %s action for foreign key constraint containing generated column",
-								"ON UPDATE")));
-			if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
-				fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("invalid %s action for foreign key constraint containing generated column",
-								"ON DELETE")));
-		}
-	}
-
 	/*
 	 * Look up the equality operators to use in the constraint.
 	 *
@@ -9950,189 +9979,68 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	for (i = 0; i < numpks; i++)
 	{
-		Oid			pktype = pktypoid[i];
-		Oid			fktype = fktypoid[i];
-		Oid			fktyped;
-		HeapTuple	cla_ht;
-		Form_pg_opclass cla_tup;
-		Oid			amid;
-		Oid			opfamily;
-		Oid			opcintype;
-		Oid			pfeqop;
-		Oid			ppeqop;
-		Oid			ffeqop;
-		int16		eqstrategy;
-		Oid			pfeqop_right;
-
-		/* We need several fields out of the pg_opclass entry */
-		cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
-		if (!HeapTupleIsValid(cla_ht))
-			elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]);
-		cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
-		amid = cla_tup->opcmethod;
-		opfamily = cla_tup->opcfamily;
-		opcintype = cla_tup->opcintype;
-		ReleaseSysCache(cla_ht);
-
-		/*
-		 * Check it's a btree; currently this can never fail since no other
-		 * index AMs support unique indexes.  If we ever did have other types
-		 * of unique indexes, we'd need a way to determine which operator
-		 * strategy number is equality.  (Is it reasonable to insist that
-		 * every such index AM use btree's number for equality?)
-		 */
-		if (amid != BTREE_AM_OID)
-			elog(ERROR, "only b-tree indexes are supported for foreign keys");
-		eqstrategy = BTEqualStrategyNumber;
-
-		/*
-		 * There had better be a primary equality operator for the index.
-		 * We'll use it for PK = PK comparisons.
-		 */
-		ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
-									 eqstrategy);
-
-		if (!OidIsValid(ppeqop))
-			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
-				 eqstrategy, opcintype, opcintype, opfamily);
-
-		/*
-		 * Are there equality operators that take exactly the FK type? Assume
-		 * we should look through any domain here.
-		 */
-		fktyped = getBaseType(fktype);
+		FindFKComparisonOperators(
+				fkconstraint, tab, i, fkattnum,
+				&old_check_ok, &old_pfeqop_item,
+				pktypoid[i], fktypoid[i], opclasses[i],
+				is_temporal, false,
+				&pfeqoperators[i], &ppeqoperators[i], &ffeqoperators[i]);
+	}
+	if (is_temporal) {
+		pkattnum[numpks] = pkperiodattnum;
+		pktypoid[numpks] = pkperiodtypoid;
+		fkattnum[numpks] = fkperiodattnum;
+		fktypoid[numpks] = fkperiodtypoid;
 
-		pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
-									 eqstrategy);
-		if (OidIsValid(pfeqop))
-		{
-			pfeqop_right = fktyped;
-			ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
-										 eqstrategy);
-		}
-		else
-		{
-			/* keep compiler quiet */
-			pfeqop_right = InvalidOid;
-			ffeqop = InvalidOid;
-		}
+		FindFKComparisonOperators(
+				fkconstraint, tab, numpks, fkattnum,
+				&old_check_ok, &old_pfeqop_item,
+				pkperiodtypoid, fkperiodtypoid, opclasses[numpks],
+				is_temporal, true,
+				&pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+		numfks += 1;
+		numpks += 1;
+	}
 
-		if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
-		{
-			/*
-			 * Otherwise, look for an implicit cast from the FK type to the
-			 * opcintype, and if found, use the primary equality operator.
-			 * This is a bit tricky because opcintype might be a polymorphic
-			 * type such as ANYARRAY or ANYENUM; so what we have to test is
-			 * whether the two actual column types can be concurrently cast to
-			 * that type.  (Otherwise, we'd fail to reject combinations such
-			 * as int[] and point[].)
-			 */
-			Oid			input_typeids[2];
-			Oid			target_typeids[2];
-
-			input_typeids[0] = pktype;
-			input_typeids[1] = fktype;
-			target_typeids[0] = opcintype;
-			target_typeids[1] = opcintype;
-			if (can_coerce_type(2, input_typeids, target_typeids,
-								COERCION_IMPLICIT))
-			{
-				pfeqop = ffeqop = ppeqop;
-				pfeqop_right = opcintype;
-			}
-		}
+	/*
+	 * Check some things for generated columns.
+	 */
+	for (i = 0; i < numfks; i++)
+	{
+		char		attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;
 
-		if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
-			ereport(ERROR,
-					(errcode(ERRCODE_DATATYPE_MISMATCH),
-					 errmsg("foreign key constraint \"%s\" cannot be implemented",
-							fkconstraint->conname),
-					 errdetail("Key columns \"%s\" and \"%s\" "
-							   "are of incompatible types: %s and %s.",
-							   strVal(list_nth(fkconstraint->fk_attrs, i)),
-							   strVal(list_nth(fkconstraint->pk_attrs, i)),
-							   format_type_be(fktype),
-							   format_type_be(pktype))));
-
-		if (old_check_ok)
-		{
-			/*
-			 * When a pfeqop changes, revalidate the constraint.  We could
-			 * permit intra-opfamily changes, but that adds subtle complexity
-			 * without any concrete benefit for core types.  We need not
-			 * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
-			 */
-			old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
-			old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
-									old_pfeqop_item);
-		}
-		if (old_check_ok)
+		if (attgenerated)
 		{
-			Oid			old_fktype;
-			Oid			new_fktype;
-			CoercionPathType old_pathtype;
-			CoercionPathType new_pathtype;
-			Oid			old_castfunc;
-			Oid			new_castfunc;
-			Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
-												   fkattnum[i] - 1);
-
 			/*
-			 * Identify coercion pathways from each of the old and new FK-side
-			 * column types to the right (foreign) operand type of the pfeqop.
-			 * We may assume that pg_constraint.conkey is not changing.
-			 */
-			old_fktype = attr->atttypid;
-			new_fktype = fktype;
-			old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
-										&old_castfunc);
-			new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
-										&new_castfunc);
-
-			/*
-			 * Upon a change to the cast from the FK column to its pfeqop
-			 * operand, revalidate the constraint.  For this evaluation, a
-			 * binary coercion cast is equivalent to no cast at all.  While
-			 * type implementors should design implicit casts with an eye
-			 * toward consistency of operations like equality, we cannot
-			 * assume here that they have done so.
-			 *
-			 * A function with a polymorphic argument could change behavior
-			 * arbitrarily in response to get_fn_expr_argtype().  Therefore,
-			 * when the cast destination is polymorphic, we only avoid
-			 * revalidation if the input type has not changed at all.  Given
-			 * just the core data types and operator classes, this requirement
-			 * prevents no would-be optimizations.
-			 *
-			 * If the cast converts from a base type to a domain thereon, then
-			 * that domain type must be the opcintype of the unique index.
-			 * Necessarily, the primary key column must then be of the domain
-			 * type.  Since the constraint was previously valid, all values on
-			 * the foreign side necessarily exist on the primary side and in
-			 * turn conform to the domain.  Consequently, we need not treat
-			 * domains specially here.
-			 *
-			 * Since we require that all collations share the same notion of
-			 * equality (which they do, because texteq reduces to bitwise
-			 * equality), we don't compare collation here.
-			 *
-			 * We need not directly consider the PK type.  It's necessarily
-			 * binary coercible to the opcintype of the unique index column,
-			 * and ri_triggers.c will only deal with PK datums in terms of
-			 * that opcintype.  Changing the opcintype also changes pfeqop.
+			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
 			 */
-			old_check_ok = (new_pathtype == old_pathtype &&
-							new_castfunc == old_castfunc &&
-							(!IsPolymorphicType(pfeqop_right) ||
-							 new_fktype == old_fktype));
+			if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
+				fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT ||
+				fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("invalid %s action for foreign key constraint containing generated column",
+								"ON UPDATE")));
+			if (fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
+				fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("invalid %s action for foreign key constraint containing generated column",
+								"ON DELETE")));
 		}
-
-		pfeqoperators[i] = pfeqop;
-		ppeqoperators[i] = ppeqop;
-		ffeqoperators[i] = ffeqop;
 	}
 
+	/*
+	 * For FKs with PERIOD we need an operator and aggregate function
+	 * to check whether the referencing row's range is contained
+	 * by the aggregated ranges of the referenced row(s).
+	 * For rangetypes this is fk.periodatt <@ range_agg(pk.periodatt).
+	 * FKs will look these up at "runtime", but we should make sure
+	 * the lookup works here.
+	 */
+	if (is_temporal)
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid, &periodprocoid);
+
 	/*
 	 * Create all the constraint and trigger objects, recursing to partitions
 	 * as necessary.  First handle the referenced side.
@@ -10149,7 +10057,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
 	/* Now handle the referencing side. */
 	addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -10165,7 +10074,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 							fkdelsetcols,
 							old_check_ok,
 							lockmode,
-							InvalidOid, InvalidOid);
+							InvalidOid, InvalidOid,
+							is_temporal);
 
 	/*
 	 * Done.  Close pk table, but keep lock until we've committed.
@@ -10250,7 +10160,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 					   Oid *ppeqoperators, Oid *ffeqoperators,
 					   int numfkdelsetcols, int16 *fkdelsetcols,
 					   bool old_check_ok,
-					   Oid parentDelTrigger, Oid parentUpdTrigger)
+					   Oid parentDelTrigger, Oid parentUpdTrigger,
+					   bool is_temporal)
 {
 	ObjectAddress address;
 	Oid			constrOid;
@@ -10336,7 +10247,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10412,7 +10323,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 								   pfeqoperators, ppeqoperators, ffeqoperators,
 								   numfkdelsetcols, fkdelsetcols,
 								   old_check_ok,
-								   deleteTriggerOid, updateTriggerOid);
+								   deleteTriggerOid, updateTriggerOid,
+								   is_temporal);
 
 			/* Done -- clean up (but keep the lock) */
 			table_close(partRel, NoLock);
@@ -10470,7 +10382,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 						Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 						int numfkdelsetcols, int16 *fkdelsetcols,
 						bool old_check_ok, LOCKMODE lockmode,
-						Oid parentInsTrigger, Oid parentUpdTrigger)
+						Oid parentInsTrigger, Oid parentUpdTrigger,
+						bool is_temporal)
 {
 	Oid			insertTriggerOid,
 				updateTriggerOid;
@@ -10518,6 +10431,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 			newcon->refrelid = RelationGetRelid(pkrel);
 			newcon->refindid = indexOid;
 			newcon->conid = parentConstr;
+			newcon->conwithperiod = fkconstraint->fk_period != NULL;
 			newcon->qual = (Node *) fkconstraint;
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10635,7 +10549,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10666,7 +10580,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10902,7 +10817,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10995,6 +10911,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -11110,6 +11027,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
 		indexOid = constrForm->conindid;
+		is_temporal = constrForm->conwithoutoverlaps;
 		constrOid =
 			CreateConstraintEntry(fkconstraint->conname,
 								  constrForm->connamespace,
@@ -11141,7 +11059,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
-								  false,	/* conWithoutOverlaps */
+								  is_temporal,	/* conWithoutOverlaps */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -11175,13 +11093,300 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								is_temporal);
 		table_close(pkrel, NoLock);
 	}
 
 	table_close(trigrel, RowExclusiveLock);
 }
 
+static void
+FindFKComparisonOperators(Constraint *fkconstraint,
+		AlteredTableInfo *tab,
+		int i,
+		int16 *fkattnum,
+		bool *old_check_ok,
+		ListCell **old_pfeqop_item,
+		Oid pktype, Oid fktype, Oid opclass,
+		bool is_temporal, bool for_overlaps,
+		Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut)
+{
+	Oid			fktyped;
+	HeapTuple	cla_ht;
+	Form_pg_opclass cla_tup;
+	Oid			amid;
+	Oid			opfamily;
+	Oid			opcintype;
+	Oid			pfeqop;
+	Oid			ppeqop;
+	Oid			ffeqop;
+	StrategyNumber	rtstrategy;
+	int16		eqstrategy;
+	Oid			pfeqop_right;
+	char	   *stratname;
+
+	/* We need several fields out of the pg_opclass entry */
+	cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
+	if (!HeapTupleIsValid(cla_ht))
+		elog(ERROR, "cache lookup failed for opclass %u", opclass);
+	cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
+	amid = cla_tup->opcmethod;
+	opfamily = cla_tup->opcfamily;
+	opcintype = cla_tup->opcintype;
+	ReleaseSysCache(cla_ht);
+
+	if (is_temporal)
+	{
+		if (amid != GIST_AM_OID)
+			elog(ERROR, "only GiST indexes are supported for temporal foreign keys");
+		/*
+		 * For the non-overlaps parts, we want either RTEqualStrategyNumber (without btree_gist)
+		 * or BTEqualStrategyNumber (with btree_gist). We'll try the latter first.
+		 */
+		if (for_overlaps)
+		{
+			stratname = "overlaps";
+			rtstrategy = RTOverlapStrategyNumber;
+		}
+		else
+		{
+			stratname = "equality";
+			rtstrategy = RTEqualStrategyNumber;
+		}
+		eqstrategy = GistTranslateStratnum(opclass, rtstrategy);
+		if (eqstrategy == InvalidStrategy)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("no %s operator found for foreign key", stratname),
+					 errdetail("Could not translate strategy number %d for opclass %d.",
+						 rtstrategy, opclass),
+					 errhint("Define a stratnum support function for your GiST opclass.")));
+	}
+	else
+	{
+		/*
+		 * Check it's a btree; currently this can never fail since no other
+		 * index AMs support unique indexes.  If we ever did have other types
+		 * of unique indexes, we'd need a way to determine which operator
+		 * strategy number is equality.  (Is it reasonable to insist that
+		 * every such index AM use btree's number for equality?)
+		 */
+		if (amid != BTREE_AM_OID)
+			elog(ERROR, "only b-tree indexes are supported for foreign keys");
+		eqstrategy = BTEqualStrategyNumber;
+	}
+
+	/*
+	 * There had better be a primary equality operator for the index.
+	 * We'll use it for PK = PK comparisons.
+	 */
+	ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
+								 eqstrategy);
+
+	if (!OidIsValid(ppeqop))
+		elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
+			 eqstrategy, opcintype, opcintype, opfamily);
+
+	/*
+	 * Are there equality operators that take exactly the FK type? Assume
+	 * we should look through any domain here.
+	 */
+	fktyped = getBaseType(fktype);
+
+	pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
+								 eqstrategy);
+	if (OidIsValid(pfeqop))
+	{
+		pfeqop_right = fktyped;
+		ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
+									 eqstrategy);
+	}
+	else
+	{
+		/* keep compiler quiet */
+		pfeqop_right = InvalidOid;
+		ffeqop = InvalidOid;
+	}
+
+	if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+	{
+		/*
+		 * Otherwise, look for an implicit cast from the FK type to the
+		 * opcintype, and if found, use the primary equality operator.
+		 * This is a bit tricky because opcintype might be a polymorphic
+		 * type such as ANYARRAY or ANYENUM; so what we have to test is
+		 * whether the two actual column types can be concurrently cast to
+		 * that type.  (Otherwise, we'd fail to reject combinations such
+		 * as int[] and point[].)
+		 */
+		Oid			input_typeids[2];
+		Oid			target_typeids[2];
+
+		input_typeids[0] = pktype;
+		input_typeids[1] = fktype;
+		target_typeids[0] = opcintype;
+		target_typeids[1] = opcintype;
+		if (can_coerce_type(2, input_typeids, target_typeids,
+							COERCION_IMPLICIT))
+		{
+			pfeqop = ffeqop = ppeqop;
+			pfeqop_right = opcintype;
+		}
+	}
+
+	if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
+	{
+		char *fkattr_name;
+		char *pkattr_name;
+
+		if (for_overlaps)
+		{
+			fkattr_name = strVal(fkconstraint->fk_period);
+			pkattr_name = strVal(fkconstraint->pk_period);
+		}
+		else
+		{
+			fkattr_name = strVal(list_nth(fkconstraint->fk_attrs, i));
+			pkattr_name = strVal(list_nth(fkconstraint->pk_attrs, i));
+		}
+
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("foreign key constraint \"%s\" cannot be implemented",
+						fkconstraint->conname),
+				 errdetail("Key columns \"%s\" and \"%s\" "
+						   "are of incompatible types: %s and %s.",
+						   fkattr_name,
+						   pkattr_name,
+						   format_type_be(fktype),
+						   format_type_be(pktype))));
+	}
+
+	if (*old_check_ok)
+	{
+		/*
+		 * When a pfeqop changes, revalidate the constraint.  We could
+		 * permit intra-opfamily changes, but that adds subtle complexity
+		 * without any concrete benefit for core types.  We need not
+		 * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
+		 */
+		*old_check_ok = (pfeqop == lfirst_oid(*old_pfeqop_item));
+		*old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
+								*old_pfeqop_item);
+	}
+	if (*old_check_ok)
+	{
+		Oid			old_fktype;
+		Oid			new_fktype;
+		CoercionPathType old_pathtype;
+		CoercionPathType new_pathtype;
+		Oid			old_castfunc;
+		Oid			new_castfunc;
+		Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
+											   fkattnum[i] - 1);
+
+		/*
+		 * Identify coercion pathways from each of the old and new FK-side
+		 * column types to the right (foreign) operand type of the pfeqop.
+		 * We may assume that pg_constraint.conkey is not changing.
+		 */
+		old_fktype = attr->atttypid;
+		new_fktype = fktype;
+		old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
+									&old_castfunc);
+		new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
+									&new_castfunc);
+
+		/*
+		 * Upon a change to the cast from the FK column to its pfeqop
+		 * operand, revalidate the constraint.  For this evaluation, a
+		 * binary coercion cast is equivalent to no cast at all.  While
+		 * type implementors should design implicit casts with an eye
+		 * toward consistency of operations like equality, we cannot
+		 * assume here that they have done so.
+		 *
+		 * A function with a polymorphic argument could change behavior
+		 * arbitrarily in response to get_fn_expr_argtype().  Therefore,
+		 * when the cast destination is polymorphic, we only avoid
+		 * revalidation if the input type has not changed at all.  Given
+		 * just the core data types and operator classes, this requirement
+		 * prevents no would-be optimizations.
+		 *
+		 * If the cast converts from a base type to a domain thereon, then
+		 * that domain type must be the opcintype of the unique index.
+		 * Necessarily, the primary key column must then be of the domain
+		 * type.  Since the constraint was previously valid, all values on
+		 * the foreign side necessarily exist on the primary side and in
+		 * turn conform to the domain.  Consequently, we need not treat
+		 * domains specially here.
+		 *
+		 * Since we require that all collations share the same notion of
+		 * equality (which they do, because texteq reduces to bitwise
+		 * equality), we don't compare collation here.
+		 *
+		 * We need not directly consider the PK type.  It's necessarily
+		 * binary coercible to the opcintype of the unique index column,
+		 * and ri_triggers.c will only deal with PK datums in terms of
+		 * that opcintype.  Changing the opcintype also changes pfeqop.
+		 */
+		*old_check_ok = (new_pathtype == old_pathtype &&
+						new_castfunc == old_castfunc &&
+						(!IsPolymorphicType(pfeqop_right) ||
+						 new_fktype == old_fktype));
+
+	}
+
+	*pfeqopOut = pfeqop;
+	*ppeqopOut = ppeqop;
+	*ffeqopOut = ffeqop;
+}
+
+/*
+ * FindFkPeriodOpersAndProcs -
+ *
+ * Looks up the oper and proc oids for confkperiodoperoids and confkperiodprocoids.
+ * These are used by foreign keys with a PERIOD element.
+ */
+void
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *periodoperoid,
+						  Oid *periodprocoid)
+{
+	Oid	opfamily;
+	Oid	opcintype;
+	Oid	aggrettype;
+	Oid	funcid = InvalidOid;
+	StrategyNumber strat = RTContainedByStrategyNumber;
+
+	/* First look up the support proc for aggregation. */
+	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+		funcid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_REFERENCED_AGG_PROC);
+
+	if (!OidIsValid(funcid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("no support func %u found for FOREIGN KEY constraint", GIST_REFERENCED_AGG_PROC),
+				 errhint("Define a referencedagg support function for your GiST opclass.")));
+
+	*periodprocoid = funcid;
+
+	/* Look up the function's rettype. */
+	aggrettype = get_func_rettype(funcid);
+
+	/*
+	 * Now look up the ContainedBy operator.
+	 * Its left arg must be the type of the column (or rather of the opclass).
+	 * Its right arg must match the return type of the support proc.
+	 */
+	GetOperatorFromWellKnownStrategy(opclass,
+									 aggrettype,
+									 "contained by",
+									 "FOREIGN KEY constraint",
+									 periodoperoid,
+									 &strat);
+}
+
 /*
  * When the parent of a partition receives [the referencing side of] a foreign
  * key, we must propagate that foreign key to the partition.  However, the
@@ -11679,7 +11884,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 			if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
 				tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
 				tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
-				tgform->tgfoid != F_RI_FKEY_CHECK_UPD)
+				tgform->tgfoid != F_RI_FKEY_CHECK_UPD &&
+				tgform->tgfoid != F_TRI_FKEY_NOACTION_DEL &&
+				tgform->tgfoid != F_TRI_FKEY_NOACTION_UPD &&
+				tgform->tgfoid != F_TRI_FKEY_CHECK_INS &&
+				tgform->tgfoid != F_TRI_FKEY_CHECK_UPD)
 				continue;
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11926,7 +12135,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11992,7 +12200,9 @@ transformColumnNameList(Oid relId, List *colList,
 static int
 transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 						   List **attnamelist,
+						   Node **pk_period,
 						   int16 *attnums, Oid *atttypids,
+						   int16 *periodattnums, Oid *periodatttypids,
 						   Oid *opclasses)
 {
 	List	   *indexoidlist;
@@ -12057,36 +12267,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 	 * Now build the list of PK attributes from the indkey definition (we
-	 * assume a primary key cannot have expressional elements)
+	 * assume a primary key cannot have expressional elements, unless it
+	 * has a PERIOD)
 	 */
 	*attnamelist = NIL;
 	for (i = 0; i < indexStruct->indnkeyatts; i++)
 	{
 		int			pkattno = indexStruct->indkey.values[i];
 
-		attnums[i] = pkattno;
-		atttypids[i] = attnumTypeId(pkrel, pkattno);
-		opclasses[i] = indclass->values[i];
-		*attnamelist = lappend(*attnamelist,
-							   makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+		if (i == indexStruct->indnkeyatts - 1 && indexStruct->indisexclusion)
+		{
+			/* we have a range */
+			/* The caller will set attnums[i] */
+			periodattnums[0] = pkattno;
+			periodatttypids[0] = attnumTypeId(pkrel, pkattno);
+			opclasses[i] = indclass->values[i];
+
+			Assert(*pk_period == NULL);
+			*pk_period = (Node *) makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno))));
+		}
+		else
+		{
+			attnums[i] = pkattno;
+			atttypids[i] = attnumTypeId(pkrel, pkattno);
+			opclasses[i] = indclass->values[i];
+			*attnamelist = lappend(*attnamelist,
+								  makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
+		}
 	}
 
 	ReleaseSysCache(indexTuple);
 
-	return i;
+	if (indexStruct->indisexclusion) return i - 1;
+	else return i;
 }
 
 /*
  * transformFkeyCheckAttrs -
  *
  *	Make sure that the attributes of a referenced table belong to a unique
- *	(or primary key) constraint.  Return the OID of the index supporting
- *	the constraint, as well as the opclasses associated with the index
+ *	(or primary key) constraint.  Or if this is a temporal foreign key
+ *	the primary key should be an exclusion constraint instead.
+ *	Return the OID of the index supporting the constraint,
+ *	as well as the opclasses associated with the index
  *	columns.
  */
 static Oid
 transformFkeyCheckAttrs(Relation pkrel,
 						int numattrs, int16 *attnums,
+						bool is_temporal, int16 periodattnum,
 						Oid *opclasses) /* output parameter */
 {
 	Oid			indexoid = InvalidOid;
@@ -12113,6 +12342,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 						(errcode(ERRCODE_INVALID_FOREIGN_KEY),
 						 errmsg("foreign key referenced-columns list must not contain duplicates")));
 		}
+		if (is_temporal && attnums[i] == periodattnum)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_FOREIGN_KEY),
+					 errmsg("foreign key referenced-columns list must not contain duplicates")));
 	}
 
 	/*
@@ -12134,12 +12367,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
-		 * Must have the right number of columns; must be unique and not a
-		 * partial index; forget it if there are any expressions, too. Invalid
-		 * indexes are out as well.
+		 * Must have the right number of columns; must be unique
+		 * (or if temporal then exclusion instead) and not a
+		 * partial index; forget it if there are any expressions, too.
+		 * Invalid indexes are out as well.
 		 */
-		if (indexStruct->indnkeyatts == numattrs &&
-			indexStruct->indisunique &&
+		if ((is_temporal
+			  ? (indexStruct->indnkeyatts == numattrs + 1 &&
+				 indexStruct->indisexclusion)
+			  : (indexStruct->indnkeyatts == numattrs &&
+				 indexStruct->indisunique)) &&
 			indexStruct->indisvalid &&
 			heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) &&
 			heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -12177,6 +12414,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 				if (!found)
 					break;
 			}
+			if (found && is_temporal)
+			{
+				found = false;
+				for (j = 0; j < numattrs + 1; j++)
+				{
+					if (periodattnum == indexStruct->indkey.values[j])
+					{
+						opclasses[numattrs] = indclass->values[j];
+						found = true;
+						break;
+					}
+				}
+			}
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12286,7 +12536,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12315,8 +12566,10 @@ validateForeignKeyConstraint(char *conname,
 	/*
 	 * See if we can do it with a single LEFT JOIN query.  A false result
 	 * indicates we must proceed with the fire-the-trigger method.
+	 * We can't do a LEFT JOIN for temporal FKs yet,
+	 * but we can once we support temporal left joins.
 	 */
-	if (RI_Initial_Check(&trig, rel, pkrel))
+	if (!temporal && RI_Initial_Check(&trig, rel, pkrel))
 		return;
 
 	/*
@@ -12385,6 +12638,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
 	 * Note: for a self-referential FK (referencing and referenced tables are
@@ -12404,12 +12658,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 	/* Either ON INSERT or ON UPDATE */
 	if (on_insert)
 	{
-		fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
+		if (is_temporal)
+			fk_trigger->funcname = SystemFuncName("TRI_FKey_check_ins");
+		else
+			fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
 		fk_trigger->events = TRIGGER_TYPE_INSERT;
 	}
 	else
 	{
-		fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
+		if (is_temporal)
+			fk_trigger->funcname = SystemFuncName("TRI_FKey_check_upd");
+		else
+			fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
 		fk_trigger->events = TRIGGER_TYPE_UPDATE;
 	}
 
@@ -12467,37 +12727,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
-	switch (fkconstraint->fk_del_action)
+	if (fkconstraint->fk_period != NULL)
 	{
-		case FKCONSTR_ACTION_NOACTION:
-			fk_trigger->deferrable = fkconstraint->deferrable;
-			fk_trigger->initdeferred = fkconstraint->initdeferred;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
-			break;
-		case FKCONSTR_ACTION_RESTRICT:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
-			break;
-		case FKCONSTR_ACTION_CASCADE:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			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");
-			break;
-		case FKCONSTR_ACTION_SETDEFAULT:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
-			break;
-		default:
-			elog(ERROR, "unrecognized FK action type: %d",
-				 (int) fkconstraint->fk_del_action);
-			break;
+		/* Temporal foreign keys */
+		switch (fkconstraint->fk_del_action)
+		{
+			case FKCONSTR_ACTION_NOACTION:
+				fk_trigger->deferrable = fkconstraint->deferrable;
+				fk_trigger->initdeferred = fkconstraint->initdeferred;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_del");
+				break;
+			case FKCONSTR_ACTION_RESTRICT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
+				break;
+			case FKCONSTR_ACTION_CASCADE:
+			case FKCONSTR_ACTION_SETNULL:
+			case FKCONSTR_ACTION_SETDEFAULT:
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("action not supported for temporal foreign keys")));
+				break;
+			default:
+				elog(ERROR, "unrecognized FK action type: %d",
+					 (int) fkconstraint->fk_del_action);
+				break;
+		}
+	}
+	else
+	{
+		switch (fkconstraint->fk_del_action)
+		{
+			case FKCONSTR_ACTION_NOACTION:
+				fk_trigger->deferrable = fkconstraint->deferrable;
+				fk_trigger->initdeferred = fkconstraint->initdeferred;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
+				break;
+			case FKCONSTR_ACTION_RESTRICT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
+				break;
+			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				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");
+				break;
+			case FKCONSTR_ACTION_SETDEFAULT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
+				break;
+			default:
+				elog(ERROR, "unrecognized FK action type: %d",
+					 (int) fkconstraint->fk_del_action);
+				break;
+		}
 	}
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
@@ -12527,37 +12818,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
-	switch (fkconstraint->fk_upd_action)
+	if (fkconstraint->fk_period != NULL)
 	{
-		case FKCONSTR_ACTION_NOACTION:
-			fk_trigger->deferrable = fkconstraint->deferrable;
-			fk_trigger->initdeferred = fkconstraint->initdeferred;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
-			break;
-		case FKCONSTR_ACTION_RESTRICT:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
-			break;
-		case FKCONSTR_ACTION_CASCADE:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			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");
-			break;
-		case FKCONSTR_ACTION_SETDEFAULT:
-			fk_trigger->deferrable = false;
-			fk_trigger->initdeferred = false;
-			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
-			break;
-		default:
-			elog(ERROR, "unrecognized FK action type: %d",
-				 (int) fkconstraint->fk_upd_action);
-			break;
+		/* Temporal foreign keys */
+		switch (fkconstraint->fk_upd_action)
+		{
+			case FKCONSTR_ACTION_NOACTION:
+				fk_trigger->deferrable = fkconstraint->deferrable;
+				fk_trigger->initdeferred = fkconstraint->initdeferred;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_upd");
+				break;
+			case FKCONSTR_ACTION_RESTRICT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
+				break;
+			case FKCONSTR_ACTION_CASCADE:
+			case FKCONSTR_ACTION_SETNULL:
+			case FKCONSTR_ACTION_SETDEFAULT:
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("action not supported for temporal foreign keys")));
+				break;
+			default:
+				elog(ERROR, "unrecognized FK action type: %d",
+					 (int) fkconstraint->fk_upd_action);
+				break;
+		}
+	}
+	else
+	{
+		switch (fkconstraint->fk_upd_action)
+		{
+			case FKCONSTR_ACTION_NOACTION:
+				fk_trigger->deferrable = fkconstraint->deferrable;
+				fk_trigger->initdeferred = fkconstraint->initdeferred;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
+				break;
+			case FKCONSTR_ACTION_RESTRICT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
+				break;
+			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				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");
+				break;
+			case FKCONSTR_ACTION_SETDEFAULT:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
+				break;
+			default:
+				elog(ERROR, "unrecognized FK action type: %d",
+					 (int) fkconstraint->fk_upd_action);
+				break;
+		}
 	}
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 03f67b68506..2adee6d5db2 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -794,7 +794,9 @@ _outConstraint(StringInfo str, const Constraint *node)
 			appendStringInfoString(str, "FOREIGN_KEY");
 			WRITE_NODE_FIELD(pktable);
 			WRITE_NODE_FIELD(fk_attrs);
+			WRITE_NODE_FIELD(fk_period);
 			WRITE_NODE_FIELD(pk_attrs);
+			WRITE_NODE_FIELD(pk_period);
 			WRITE_CHAR_FIELD(fk_matchtype);
 			WRITE_CHAR_FIELD(fk_upd_action);
 			WRITE_CHAR_FIELD(fk_del_action);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index cfb552fde74..bff835b4c18 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -462,7 +462,9 @@ _readConstraint(void)
 		case CONSTR_FOREIGN:
 			READ_NODE_FIELD(pktable);
 			READ_NODE_FIELD(fk_attrs);
+			READ_NODE_FIELD(fk_period);
 			READ_NODE_FIELD(pk_attrs);
+			READ_NODE_FIELD(pk_period);
 			READ_CHAR_FIELD(fk_matchtype);
 			READ_CHAR_FIELD(fk_upd_action);
 			READ_CHAR_FIELD(fk_del_action);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 130f7fc7c3f..a1182042b86 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -522,12 +522,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				 SetResetClause FunctionSetResetClause
 
 %type <node>	TableElement TypedTableElement ConstraintElem TableFuncElement
-%type <node>	columnDef columnOptions
+%type <node>	columnDef columnOptions optionalPeriodName
 %type <defelt>	def_elem reloption_elem old_aggr_elem operator_def_elem
 %type <node>	def_arg columnElem where_clause where_or_current_clause
 				a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
 				columnref in_expr having_clause func_table xmltable array_expr
 				OptWhereClause operator_def_arg
+%type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
 %type <boolean> opt_ordinality opt_without_overlaps
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
@@ -745,7 +746,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
-	PLACING PLANS POLICY
+	PERIOD PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -4222,21 +4223,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
-			| FOREIGN KEY '(' columnList ')' REFERENCES qualified_name
-				opt_column_list key_match key_actions ConstraintAttributeSpec
+			| FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name
+				opt_column_and_period_list key_match key_actions ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
 
 					n->contype = CONSTR_FOREIGN;
 					n->location = @1;
-					n->pktable = $7;
+					n->pktable = $8;
 					n->fk_attrs = $4;
-					n->pk_attrs = $8;
-					n->fk_matchtype = $9;
-					n->fk_upd_action = ($10)->updateAction->action;
-					n->fk_del_action = ($10)->deleteAction->action;
-					n->fk_del_set_cols = ($10)->deleteAction->cols;
-					processCASbits($11, @11, "FOREIGN KEY",
+					n->fk_period = $5;
+					n->pk_attrs = linitial($9);
+					n->pk_period = lsecond($9);
+					n->fk_matchtype = $10;
+					n->fk_upd_action = ($11)->updateAction->action;
+					n->fk_del_action = ($11)->deleteAction->action;
+					n->fk_del_set_cols = ($11)->deleteAction->cols;
+					processCASbits($12, @12, "FOREIGN KEY",
 								   &n->deferrable, &n->initdeferred,
 								   &n->skip_validation, NULL,
 								   yyscanner);
@@ -4264,6 +4267,16 @@ columnList:
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
 		;
 
+optionalPeriodName:
+			',' PERIOD columnElem { $$ = $3; }
+			| /*EMPTY*/               { $$ = NULL; }
+	;
+
+opt_column_and_period_list:
+			'(' columnList optionalPeriodName ')'			{ $$ = list_make2($2, $3); }
+			| /*EMPTY*/								{ $$ = list_make2(NIL, NULL); }
+		;
+
 columnElem: ColId
 				{
 					$$ = (Node *) makeString($1);
@@ -17618,6 +17631,7 @@ reserved_keyword:
 			| ONLY
 			| OR
 			| ORDER
+			| PERIOD
 			| PLACING
 			| PRIMARY
 			| REFERENCES
@@ -17927,6 +17941,7 @@ bare_label_keyword:
 			| PARTITION
 			| PASSING
 			| PASSWORD
+			| PERIOD
 			| PLACING
 			| PLANS
 			| POLICY
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 708d9c8efc9..0dadf2acb2d 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -31,7 +31,9 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_range.h"
 #include "catalog/pg_type.h"
+#include "commands/tablecmds.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
@@ -48,6 +50,7 @@
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/ruleutils.h"
@@ -118,12 +121,15 @@ typedef struct RI_ConstraintInfo
 	int16		confdelsetcols[RI_MAX_NUMKEYS]; /* attnums of cols to set on
 												 * delete */
 	char		confmatchtype;	/* foreign key's match type */
+	bool		temporal;		/* if the foreign key is temporal */
 	int			nkeys;			/* number of key columns */
 	int16		pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */
 	int16		fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */
 	Oid			pf_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = FK) */
 	Oid			pp_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = PK) */
 	Oid			ff_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (FK = FK) */
+	Oid			period_contained_by_oper;	/* operator for PEROID SQL */
+	Oid			period_referenced_agg_proc;	/* proc for PERIOD SQL */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -200,8 +206,9 @@ static int	ri_NullCheck(TupleDesc tupDesc, TupleTableSlot *slot,
 static void ri_BuildQueryKey(RI_QueryKey *key,
 							 const RI_ConstraintInfo *riinfo,
 							 int32 constr_queryno);
-static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+static bool ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 						 const RI_ConstraintInfo *riinfo, bool rel_is_pk);
+static bool ri_RangeAttributeNeedsCheck(bool rel_is_pk, Datum oldvalue, Datum newvalue);
 static bool ri_AttributesEqual(Oid eq_opr, Oid typeid,
 							   Datum oldvalue, Datum newvalue);
 
@@ -231,6 +238,8 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 							   Relation pk_rel, Relation fk_rel,
 							   TupleTableSlot *violatorslot, TupleDesc tupdesc,
 							   int queryno, bool partgone) pg_attribute_noreturn();
+static void lookupTRIOperAndProc(const RI_ConstraintInfo *riinfo,
+								 char **opname, char **aggname);

 /*
@@ -361,26 +370,58 @@ RI_FKey_check(TriggerData *trigdata)
 
 		/* ----------
 		 * The query string built is
-		 *	SELECT 1 FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
-		 *		   FOR KEY SHARE OF x
+		 *	SELECT 1
+		 *	FROM [ONLY] <pktable> x WHERE pkatt1 = $1 [AND ...]
+		 *	FOR KEY SHARE OF x
 		 * The type id's for the $ parameters are those of the
 		 * corresponding FK attributes.
+		 *
+		 * But for temporal FKs we need to make sure
+		 * the FK's range is completely covered.
+		 * So we use this query instead:
+		 *  SELECT 1
+		 *	FROM	(
+		 *		SELECT pkperiodatt AS r
+		 *		FROM   [ONLY] pktable x
+		 *		WHERE  pkatt1 = $1 [AND ...]
+		 *		AND    pkperiodatt && $n
+		 *		FOR KEY SHARE OF x
+		 *	) x1
+		 *  HAVING $n <@ range_agg(x1.r)
+		 * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+		 * we can make this a bit simpler.
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
 		pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
 			"" : "ONLY ";
 		quoteRelationName(pkrelname, pk_rel);
-		appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
-						 pk_only, pkrelname);
+		if (riinfo->temporal)
+		{
+			quoteOneName(attname,
+					RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+			appendStringInfo(&querybuf,
+					"SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+					attname, pk_only, pkrelname);
+		}
+		else
+		{
+			appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+							 pk_only, pkrelname);
+		}
 		querysep = "WHERE";
 		for (int i = 0; i < riinfo->nkeys; i++)
 		{
-			Oid			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
-			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+			Oid	pk_type;
+			Oid	fk_type;
 
+			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
 			quoteOneName(attname,
 						 RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
+			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+
 			sprintf(paramname, "$%d", i + 1);
 			ri_GenerateQual(&querybuf, querysep,
 							attname, pk_type,
@@ -390,6 +431,15 @@ RI_FKey_check(TriggerData *trigdata)
 			queryoids[i] = fk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->temporal)
+		{
+			char *opname;
+			char *aggname;
+			lookupTRIOperAndProc(riinfo, &opname, &aggname);
+			appendStringInfo(&querybuf, ") x1 HAVING $%d %s %s(x1.r)", riinfo->nkeys, opname, aggname);
+			pfree(opname);
+			pfree(aggname);
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -497,21 +547,49 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 		 *		   FOR KEY SHARE OF x
 		 * The type id's for the $ parameters are those of the
 		 * PK attributes themselves.
+		 * But for temporal FKs we need to make sure
+		 * the FK's range is completely covered.
+		 * So we use this query instead:
+		 *  SELECT 1
+		 *  FROM    (
+		 *	  SELECT pkperiodatt AS r
+		 *	  FROM   [ONLY] pktable x
+		 *	  WHERE  pkatt1 = $1 [AND ...]
+		 *	  AND    pkperiodatt && $n
+		 *	  FOR KEY SHARE OF x
+		 *  ) x1
+		 *  HAVING $n <@ range_agg(x1.r)
+		 * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+		 * we can make this a bit simpler.
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
 		pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
 			"" : "ONLY ";
 		quoteRelationName(pkrelname, pk_rel);
-		appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
-						 pk_only, pkrelname);
+		if (riinfo->temporal)
+		{
+			quoteOneName(attname, RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+			appendStringInfo(&querybuf,
+					"SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+					attname, pk_only, pkrelname);
+		}
+		else
+		{
+			appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+					pk_only, pkrelname);
+		}
+
 		querysep = "WHERE";
 		for (int i = 0; i < riinfo->nkeys; i++)
 		{
-			Oid			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+			Oid		pk_type;
 
+			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
 			quoteOneName(attname,
 						 RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
 			sprintf(paramname, "$%d", i + 1);
 			ri_GenerateQual(&querybuf, querysep,
 							attname, pk_type,
@@ -521,6 +599,15 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 			queryoids[i] = pk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->temporal)
+		{
+			char *opname;
+			char *aggname;
+			lookupTRIOperAndProc(riinfo, &opname, &aggname);
+			appendStringInfo(&querybuf, ") x1 HAVING $%d %s %s(x1.r)", riinfo->nkeys, opname, aggname);
+			pfree(opname);
+			pfree(aggname);
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -695,10 +782,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 		querysep = "WHERE";
 		for (int i = 0; i < riinfo->nkeys; i++)
 		{
-			Oid			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
-			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
-			Oid			pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
-			Oid			fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
+			Oid		pk_type;
+			Oid		fk_type;
+			Oid		pk_coll;
+			Oid		fk_coll;
+
+			pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+			pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]);
+
+			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]);
+			fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]);
 
 			quoteOneName(attname,
 						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
@@ -1213,6 +1306,126 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	}
 }
 
+/* ----------
+ * TRI_FKey_check_ins -
+ *
+ *	Check temporal foreign key existence at insert event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_ins(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Check that this is a valid trigger call on the right time and event.
+	 */
+	ri_CheckTrigger(fcinfo, "RI_FKey_check_ins", RI_TRIGTYPE_INSERT);
+
+	/*
+	 * Share code with UPDATE case.
+	 */
+	return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_check_upd -
+ *
+ *	Check temporal foreign key existence at update event on FK table.
+ * ----------
+ */
+Datum
+TRI_FKey_check_upd(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Check that this is a valid trigger call on the right time and event.
+	 */
+	ri_CheckTrigger(fcinfo, "RI_FKey_check_upd", RI_TRIGTYPE_UPDATE);
+
+	/*
+	 * Share code with INSERT case.
+	 */
+	return RI_FKey_check((TriggerData *) fcinfo->context);
+}
+
+
+/* ----------
+ * TRI_FKey_noaction_del -
+ *
+ *	Give an error and roll back the current transaction if the
+ *	delete has resulted in a violation of the given temporal
+ *	referential integrity constraint.
+ * ----------
+ */
+Datum
+TRI_FKey_noaction_del(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Check that this is a valid trigger call on the right time and event.
+	 */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_del", RI_TRIGTYPE_DELETE);
+
+	/*
+	 * Share code with RESTRICT/UPDATE cases.
+	 */
+	return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_del -
+ *
+ * Restrict delete from PK table to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the delete is performed, rather than after.  This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT".  In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_del", RI_TRIGTYPE_DELETE);
+
+	/* Share code with NO ACTION/UPDATE cases. */
+	return ri_restrict((TriggerData *) fcinfo->context, false);
+}
+
+/*
+ * TRI_FKey_noaction_upd -
+ *
+ * Give an error and roll back the current transaction if the
+ * update has resulted in a violation of the given referential
+ * integrity constraint.
+ */
+Datum
+TRI_FKey_noaction_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with RESTRICT/DELETE cases. */
+	return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_upd -
+ *
+ * Restrict update of PK to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the update is performed, rather than after.  This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT".  In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with NO ACTION/DELETE cases. */
+	return ri_restrict((TriggerData *) fcinfo->context, false);
+}
 
 /*
  * RI_FKey_pk_upd_check_required -
@@ -1241,7 +1454,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
 		return false;
 
 	/* If all old and new key values are equal, no check is needed */
-	if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true))
+	if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true))
 		return false;
 
 	/* Else we need to fire the trigger. */
@@ -1340,7 +1553,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
 		return true;
 
 	/* If all old and new key values are equal, no check is needed */
-	if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false))
+	if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false))
 		return false;
 
 	/* Else we need to fire the trigger. */
@@ -1488,15 +1701,17 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
 	sep = "(";
 	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]);
+		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(pkattname + 3,
 					 RIAttName(pk_rel, riinfo->pk_attnums[i]));
+
 		quoteOneName(fkattname + 3,
 					 RIAttName(fk_rel, riinfo->fk_attnums[i]));
+
 		ri_GenerateQual(&querybuf, sep,
 						pkattname, pk_type,
 						riinfo->pf_eq_oprs[i],
@@ -2137,6 +2352,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache,
 											   &constraintOid,
 											   HASH_ENTER, &found);
+
 	if (!found)
 		riinfo->valid = false;
 	else if (riinfo->valid)
@@ -2171,6 +2387,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	riinfo->confupdtype = conForm->confupdtype;
 	riinfo->confdeltype = conForm->confdeltype;
 	riinfo->confmatchtype = conForm->confmatchtype;
+	riinfo->temporal = conForm->conwithoutoverlaps;
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2182,6 +2399,18 @@ ri_LoadConstraintInfo(Oid constraintOid)
 							   &riinfo->ndelsetcols,
 							   riinfo->confdelsetcols);
 
+	/*
+	 * For temporal FKs, get the operator and aggregate function we need.
+	 * We ask the opclass of the PK element for this.
+	 * This all gets cached (as does the generated plan),
+	 * so there's no performance issue.
+	 */
+	if (riinfo->temporal)
+	{
+		Oid	opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);
+		FindFKPeriodOpersAndProcs(opclass, &riinfo->period_contained_by_oper, &riinfo->period_referenced_agg_proc);
+	}
+
 	ReleaseSysCache(tup);
 
 	/*
@@ -2791,9 +3020,12 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)

 /*
- * ri_KeysEqual -
+ * ri_KeysStable -
  *
- * Check if all key values in OLD and NEW are equal.
+ * Check if all key values in OLD and NEW are "equivalent":
+ * For normal FKs we check for equality.
+ * For temporal FKs we check that the PK side is a superset of its old value,
+ * or the FK side is a subset.
  *
  * Note: at some point we might wish to redefine this as checking for
  * "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be
@@ -2801,7 +3033,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
  * previously found at least one of the rows to contain no nulls.
  */
 static bool
-ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
+ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 			 const RI_ConstraintInfo *riinfo, bool rel_is_pk)
 {
 	const int16 *attnums;
@@ -2834,35 +3066,86 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 
 		if (rel_is_pk)
 		{
-			/*
-			 * If we are looking at the PK table, then do a bytewise
-			 * comparison.  We must propagate PK changes if the value is
-			 * changed to one that "looks" different but would compare as
-			 * equal using the equality operator.  This only makes a
-			 * difference for ON UPDATE CASCADE, but for consistency we treat
-			 * all changes to the PK the same.
-			 */
-			Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
+			if (riinfo->temporal && i == riinfo->nkeys - 1)
+			{
+				if (ri_RangeAttributeNeedsCheck(true, oldvalue, newvalue))
+					return false;
+			}
+			else
+			{
+				/*
+				 * If we are looking at the PK table, then do a bytewise
+				 * comparison.  We must propagate PK changes if the value is
+				 * changed to one that "looks" different but would compare as
+				 * equal using the equality operator.  This only makes a
+				 * difference for ON UPDATE CASCADE, but for consistency we treat
+				 * all changes to the PK the same.
+				 */
+				Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
 
-			if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
-				return false;
+				if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
+					return false;
+			}
 		}
 		else
 		{
-			/*
-			 * For the FK table, compare with the appropriate equality
-			 * operator.  Changes that compare equal will still satisfy the
-			 * constraint after the update.
-			 */
-			if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
-									oldvalue, newvalue))
-				return false;
+			if (riinfo->temporal && i == riinfo->nkeys - 1)
+			{
+				if (ri_RangeAttributeNeedsCheck(false, oldvalue, newvalue))
+					return false;
+			}
+			else
+			{
+				/*
+				 * For the FK table, compare with the appropriate equality
+				 * operator.  Changes that compare equal will still satisfy the
+				 * constraint after the update.
+				 */
+				if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
+										oldvalue, newvalue))
+					return false;
+			}
 		}
 	}
 
 	return true;
 }
 
+/*
+ * ri_RangeAttributeNeedsCheck -
+ *
+ * Compare old and new values, and return true if we need to check the FK.
+ *
+ * NB: we have already checked that neither value is null.
+ */
+static bool
+ri_RangeAttributeNeedsCheck(bool rel_is_pk, Datum oldvalue, Datum newvalue)
+{
+	RangeType *oldrange, *newrange;
+	Oid oldrngtype, newrngtype;
+	TypeCacheEntry *typcache;
+
+	oldrange = DatumGetRangeTypeP(oldvalue);
+	newrange = DatumGetRangeTypeP(newvalue);
+	oldrngtype = RangeTypeGetOid(oldrange);
+	newrngtype = RangeTypeGetOid(newrange);
+
+	if (oldrngtype != newrngtype)
+		elog(ERROR, "range types are inconsistent");
+
+	typcache = lookup_type_cache(oldrngtype, TYPECACHE_RANGE_INFO);
+	if (typcache->rngelemtype == NULL)
+		elog(ERROR, "type %u is not a range type", oldrngtype);
+
+	if (rel_is_pk)
+		/* If the PK's range shrunk, a conflict is possible. */
+		return !range_eq_internal(typcache, oldrange, newrange) &&
+			!range_contains_internal(typcache, newrange, oldrange);
+	else
+		/* If the FK's range grew, a conflict is possible. */
+		return !range_eq_internal(typcache, oldrange, newrange) &&
+			range_contains_internal(typcache, newrange, oldrange);
+}
 
 /*
  * ri_AttributesEqual -
@@ -3021,12 +3304,49 @@ 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_TRI_FKEY_RESTRICT_DEL:
+		case F_TRI_FKEY_RESTRICT_UPD:
+		case F_TRI_FKEY_NOACTION_DEL:
+		case F_TRI_FKEY_NOACTION_UPD:
 			return RI_TRIGGER_PK;
 
 		case F_RI_FKEY_CHECK_INS:
 		case F_RI_FKEY_CHECK_UPD:
+		case F_TRI_FKEY_CHECK_INS:
+		case F_TRI_FKEY_CHECK_UPD:
 			return RI_TRIGGER_FK;
 	}
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * lookupTRIOperAndProc -
+ *
+ * Gets the names of the operator and aggregate function
+ * used to build the SQL for TRI constraints.
+ * Raises an error if either is not found.
+ */
+static void
+lookupTRIOperAndProc(const RI_ConstraintInfo *riinfo, char **opname, char **aggname)
+{
+	Oid	oid;
+
+	oid = riinfo->period_contained_by_oper;
+	if (!OidIsValid(oid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("no ContainedBy operator for foreign key constraint \"%s\"",
+						NameStr(riinfo->conname)),
+				 errhint("You must use an operator class with a matching ContainedBy operator.")));
+	*opname = get_opname(oid);
+
+	oid = riinfo->period_referenced_agg_proc;
+	if (!OidIsValid(oid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("no referencedagg support function for foreign key constraint \"%s\"",
+						NameStr(riinfo->conname)),
+				 errhint("You must use an operator class with a referencedagg support function.")));
+	*aggname = get_func_name_and_namespace(oid);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b625f471a84..e896bd2bec7 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -339,7 +339,7 @@ static char *pg_get_viewdef_worker(Oid viewoid,
 								   int prettyFlags, int wrapColumn);
 static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
 static int	decompile_column_index_array(Datum column_index_array, Oid relId,
-										 StringInfo buf);
+										 bool withPeriod, StringInfo buf);
 static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
 static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
 									const Oid *excludeOps,
@@ -2247,7 +2247,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				decompile_column_index_array(val, conForm->conrelid, &buf);
+				/* If it is a temporal foreign key then it uses PERIOD. */
+				decompile_column_index_array(val, conForm->conrelid,
+											 conForm->conwithoutoverlaps, &buf);
 
 				/* add foreign relation name */
 				appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2260,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_confkey);
 
-				decompile_column_index_array(val, conForm->confrelid, &buf);
+				decompile_column_index_array(val, conForm->confrelid,
+											 conForm->conwithoutoverlaps, &buf);
 
 				appendStringInfoChar(&buf, ')');
 
@@ -2344,7 +2347,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (!isnull)
 				{
 					appendStringInfoString(&buf, " (");
-					decompile_column_index_array(val, conForm->conrelid, &buf);
+					decompile_column_index_array(val, conForm->conrelid, false, &buf);
 					appendStringInfoChar(&buf, ')');
 				}
 
@@ -2379,7 +2382,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+				keyatts = decompile_column_index_array(val, conForm->conrelid, false, &buf);
 				if (conForm->conwithoutoverlaps)
 					appendStringInfoString(&buf, " WITHOUT OVERLAPS");
 
@@ -2577,7 +2580,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  */
 static int
 decompile_column_index_array(Datum column_index_array, Oid relId,
-							 StringInfo buf)
+							 bool withPeriod, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2596,7 +2599,9 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
 		if (j == 0)
 			appendStringInfoString(buf, quote_identifier(colName));
 		else
-			appendStringInfo(buf, ", %s", quote_identifier(colName));
+			appendStringInfo(buf, ", %s%s",
+							 (withPeriod && j == nKeys - 1) ? "PERIOD " : "",
+							 quote_identifier(colName));
 	}
 
 	return nKeys;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index f730aa26c47..6cd14d4bd12 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1600,6 +1600,37 @@ get_func_name(Oid funcid)
 		return NULL;
 }
 
+/*
+ * get_func_name_and_namespace
+ *	  returns the schema-qualified name of the function with the given funcid
+ *
+ * Note: returns a palloc'd copy of the string, or NULL if no such function.
+ */
+char *
+get_func_name_and_namespace(Oid funcid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_proc functup = (Form_pg_proc) GETSTRUCT(tp);
+		char	   *namesp = get_namespace_name(functup->pronamespace);
+		char	   *func = NameStr(functup->proname);
+		char	   *result;
+		int			len = strlen(namesp) + strlen(func) + 2;
+
+		result = palloc(len * sizeof(char));
+		snprintf(result, len, "%s.%s", namesp, func);
+
+		pfree(namesp);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return NULL;
+}
+
 /*
  * get_func_namespace
  *
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 8fd76a94d78..75804d66ee7 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -127,19 +127,19 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	int16		confkey[1];
 
 	/*
-	 * If a foreign key, the OIDs of the PK = FK equality operators for each
+	 * If a foreign key, the OIDs of the PK = FK comparison operators for each
 	 * column of the constraint
 	 */
 	Oid			conpfeqop[1] BKI_LOOKUP(pg_operator);
 
 	/*
-	 * If a foreign key, the OIDs of the PK = PK equality operators for each
+	 * If a foreign key, the OIDs of the PK = PK comparison operators for each
 	 * column of the constraint (i.e., equality for the referenced columns)
 	 */
 	Oid			conppeqop[1] BKI_LOOKUP(pg_operator);
 
 	/*
-	 * If a foreign key, the OIDs of the FK = FK equality operators for each
+	 * If a foreign key, the OIDs of the FK = FK comparison operators for each
 	 * column of the constraint (i.e., equality for the referencing columns)
 	 */
 	Oid			conffeqop[1] BKI_LOOKUP(pg_operator);
@@ -181,7 +181,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg
 
 /* conkey can contain zero (InvalidAttrNumber) if a whole-row Var is used */
 DECLARE_ARRAY_FOREIGN_KEY_OPT((conrelid, conkey), pg_attribute, (attrelid, attnum));
-DECLARE_ARRAY_FOREIGN_KEY((confrelid, confkey), pg_attribute, (attrelid, attnum));
+DECLARE_ARRAY_FOREIGN_KEY_OPT((confrelid, confkey), pg_attribute, (attrelid, attnum));
 
 #ifdef EXPOSE_TO_CLIENT_CODE
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a0277e57c7d..14200460abb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3945,6 +3945,30 @@
   prorettype => 'trigger', proargtypes => '',
   prosrc => 'RI_FKey_noaction_upd' },
 
+# Temporal referential integrity constraint triggers
+{ oid => '6122', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+  proname => 'TRI_FKey_check_ins', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_check_ins' },
+{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+  proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
+  proname => 'TRI_FKey_restrict_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_restrict_del' },
+{ oid => '6127', descr => 'temporal referential integrity ON UPDATE RESTRICT',
+  proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
+  proname => 'TRI_FKey_noaction_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_noaction_del' },
+{ oid => '6133', descr => 'temporal referential integrity ON UPDATE NO ACTION',
+  proname => 'TRI_FKey_noaction_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_noaction_upd' },
+
 { oid => '1666',
   proname => 'varbiteq', proleakproof => 't', prorettype => 'bool',
   proargtypes => 'varbit varbit', prosrc => 'biteq' },
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 4352a438899..baeea439ee5 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -50,8 +50,10 @@ extern bool CheckIndexCompatible(Oid oldId,
 extern Oid	GetDefaultOpClass(Oid type_id, Oid am_id);
 extern Oid	ResolveOpClass(const List *opclass, Oid attrType,
 						   const char *accessMethodName, Oid accessMethodId);
-extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
-											 const char *opname, Oid *opid,
+extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
+											 const char *opname,
+											 const char *context,
+											 Oid *opid,
 											 StrategyNumber *strat);
 
 /* commands/functioncmds.c */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 60d9b8f5b5f..96769472bbb 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,5 +105,8 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 										 Oid relId, Oid oldRelId, void *arg);
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
+extern void FindFKPeriodOpersAndProcs(Oid opclass,
+									  Oid *periodoperoid,
+									  Oid *periodprocoid);
 
 #endif							/* TABLECMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 476d55dd240..8f4f8ce6a77 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2564,6 +2564,10 @@ typedef enum ConstrType			/* types of constraints */
 #define FKCONSTR_MATCH_PARTIAL		'p'
 #define FKCONSTR_MATCH_SIMPLE		's'
 
+/* Foreign key with PERIOD positions */
+#define FKCONSTR_PERIOD_OP_CONTAINED_BY	0
+#define FKCONSTR_PERIOD_PROC_REFERENCED_AGG 0
+
 typedef struct Constraint
 {
 	pg_node_attr(custom_read_write)
@@ -2610,7 +2614,9 @@ typedef struct Constraint
 	/* Fields used for FOREIGN KEY constraints: */
 	RangeVar   *pktable;		/* Primary key table */
 	List	   *fk_attrs;		/* Attributes of foreign key */
+	Node	   *fk_period;		/* String node naming Period or range column */
 	List	   *pk_attrs;		/* Corresponding attrs in PK table */
+	Node	   *pk_period;		/* String node naming Period or range column */
 	char		fk_matchtype;	/* FULL, PARTIAL, SIMPLE */
 	char		fk_upd_action;	/* ON UPDATE action */
 	char		fk_del_action;	/* ON DELETE action */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 2331acac091..1d3f7bd37f2 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -326,6 +326,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index e4a200b00ec..1339382f91d 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -119,6 +119,7 @@ extern Oid	get_negator(Oid opno);
 extern RegProcedure get_oprrest(Oid opno);
 extern RegProcedure get_oprjoin(Oid opno);
 extern char *get_func_name(Oid funcid);
+extern char *get_func_name_and_namespace(Oid funcid);
 extern Oid	get_func_namespace(Oid funcid);
 extern Oid	get_func_rettype(Oid funcid);
 extern int	get_func_nargs(Oid funcid);
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 726e94102bf..c633738c2eb 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -393,3 +393,564 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
 DROP TABLE temporal_partitioned;
+--
+-- test FK dependencies
+--
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ERROR:  cannot drop column valid_at of table temporal3 because other objects depend on it
+DETAIL:  constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+--
+-- test FOREIGN KEY, range references range
+--
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at int4range,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_rng2rng_fk2" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange.
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_rng (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2rng
+           Table "public.temporal_fk2_rng2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | tsrange   |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_rng2rng;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2rng
+	ADD CONSTRAINT temporal_fk2_rng2rng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2rng
+           Table "public.temporal_fk2_rng2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | tsrange   |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk,
+	ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ERROR:  foreign key constraint "temporal_fk_rng2rng_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: daterange and tsrange.
+ALTER TABLE temporal_fk_rng2rng
+	ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_rng (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK child inserts
+--
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+--
+-- test FK child updates
+--
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Tue May 01 00:00:00 2018")) is not present in table "temporal_rng".
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), ["Tue Jan 02 00:00:00 2018","Thu Mar 01 00:00:00 2018")) is not present in table "temporal_rng".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_rng VALUES
+    ('[5,5]', tsrange('2018-01-01', '2018-02-01')),
+    ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+  INSERT INTO temporal_fk_rng2rng VALUES
+    ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+--
+-- test FK parent updates NO ACTION
+--
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent updates RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test FK parent deletes RESTRICT
+--
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+--
+-- test ON UPDATE/DELETE options
+--
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+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 CASCADE ON UPDATE CASCADE;
+ERROR:  action not supported for temporal foreign keys
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+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:  action not supported for temporal foreign keys
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+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:  action not supported for temporal foreign keys
+-- FK between partitioned tables
+CREATE TABLE temporal_partitioned_rng (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+INSERT INTO temporal_partitioned_rng VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[2,2]', daterange('2000-01-01', '2010-01-01'), 'two');
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+-- partitioned FK child inserts
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
+  ('[1,1]', daterange('2001-01-01', '2002-01-01'), '[2,2]'),
+  ('[2,2]', daterange('2000-01-01', '2000-02-15'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[3,3]', daterange('2010-01-01', '2010-02-15'), '[1,1]');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [01-01-2010,02-15-2010)) is not present in table "temporal_partitioned_rng".
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[3,3]', daterange('2000-01-01', '2000-02-15'), '[3,3]');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([3,4), [01-01-2000,02-15-2000)) is not present in table "temporal_partitioned_rng".
+-- partitioned FK child updates
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,2]';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,4]' WHERE id = '[1,1]';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,1]' WHERE id = '[4,4]';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,1]';
+ERROR:  conflicting key value violates exclusion constraint "tfkp1_pkey"
+DETAIL:  Key (id, valid_at)=([1,2), [01-01-2000,04-01-2000)) conflicts with existing key (id, valid_at)=([1,2), [01-01-2000,04-01-2000)).
+-- partitioned FK parent updates NO ACTION
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+  WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent deletes NO ACTION
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent updates RESTRICT
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_partitioned_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+  WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+-- partitioned FK parent deletes RESTRICT
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
+-- partitioned FK parent updates CASCADE
+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:  action not supported for temporal foreign keys
+-- partitioned FK parent deletes CASCADE
+-- partitioned FK parent updates SET NULL
+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:  action not supported for temporal foreign keys
+-- partitioned FK parent deletes SET NULL
+-- partitioned FK parent updates SET DEFAULT
+ALTER TABLE temporal_partitioned_fk_rng2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	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 DEFAULT ON UPDATE SET DEFAULT;
+ERROR:  action not supported for temporal foreign keys
+-- partitioned FK parent deletes SET DEFAULT
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index c8e8ab994a0..641cdd5b19f 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -288,3 +288,538 @@ 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;
 DROP TABLE temporal_partitioned;
+
+--
+-- test FK dependencies
+--
+
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at tsrange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+
+--
+-- test FOREIGN KEY, range references range
+--
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at int4range,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+-- with inferred PK on the referenced table:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_rng (id, PERIOD id)
+);
+
+-- Two scalar columns
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2rng
+DROP TABLE temporal_fk2_rng2rng;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at tsrange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2rng
+	ADD CONSTRAINT temporal_fk2_rng2rng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2rng
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk,
+	ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+
+--
+-- test FK child inserts
+--
+
+INSERT INTO temporal_fk_rng2rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+-- now it should work:
+INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]');
+
+--
+-- test FK child updates
+--
+
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]';
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_rng VALUES
+    ('[5,5]', tsrange('2018-01-01', '2018-02-01')),
+    ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+  INSERT INTO temporal_fk_rng2rng VALUES
+    ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK parent updates NO ACTION
+--
+
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+
+--
+-- test FK parent updates RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_rng SET valid_at = tsrange('2016-02-01', '2016-03-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
+WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+--
+-- test FK parent deletes NO ACTION
+--
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test FK parent deletes RESTRICT
+--
+
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_rng VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+
+--
+-- test ON UPDATE/DELETE options
+--
+
+-- test FK parent updates CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+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 CASCADE ON UPDATE CASCADE;
+
+-- test FK parent updates SET NULL
+INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+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;
+
+-- test FK parent updates SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+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;
+
+-- FK between partitioned tables
+
+CREATE TABLE temporal_partitioned_rng (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+INSERT INTO temporal_partitioned_rng VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
+  ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
+  ('[2,2]', daterange('2000-01-01', '2010-01-01'), 'two');
+
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
+CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+
+-- partitioned FK child inserts
+
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
+  ('[1,1]', daterange('2001-01-01', '2002-01-01'), '[2,2]'),
+  ('[2,2]', daterange('2000-01-01', '2000-02-15'), '[1,1]');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[3,3]', daterange('2010-01-01', '2010-02-15'), '[1,1]');
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES
+  ('[3,3]', daterange('2000-01-01', '2000-02-15'), '[3,3]');
+
+-- partitioned FK child updates
+
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,2]';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,4]' WHERE id = '[1,1]';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,1]' WHERE id = '[4,4]';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,1]';
+
+-- partitioned FK parent updates NO ACTION
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent deletes NO ACTION
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent updates RESTRICT
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_partitioned_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+  WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]';
+
+-- partitioned FK parent deletes RESTRICT
+
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[5,5]', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-05', '2018-01-10'), '[5,5]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- should fail:
+DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-01-01', '2018-02-01');
+
+-- partitioned FK parent updates CASCADE
+
+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;
+
+-- partitioned FK parent deletes CASCADE
+
+-- partitioned FK parent updates SET NULL
+
+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;
+
+-- partitioned FK parent deletes SET NULL
+
+-- partitioned FK parent updates SET DEFAULT
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	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 DEFAULT ON UPDATE SET DEFAULT;
+
+-- partitioned FK parent deletes SET DEFAULT
+
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
-- 
2.42.0

From 3a8b1dc24d183fa020bc1b7dda7d7e312d0e56ee Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v23 5/8] Add {multi,}range_without_portion proc & operator

These return an array of their input type are work like minus but don't
fail on splits. They never contain empty elements. We will use this to
support FOR PORTION OF.
---
 doc/src/sgml/catalogs.sgml                    |   2 +-
 src/backend/utils/adt/multirangetypes.c       |  49 +++++++
 src/backend/utils/adt/rangetypes.c            | 124 ++++++++++++++++++
 src/include/catalog/pg_operator.dat           |   6 +
 src/include/catalog/pg_proc.dat               |   6 +
 src/include/utils/rangetypes.h                |   2 +
 src/test/regress/expected/multirangetypes.out | 121 +++++++++++++++++
 src/test/regress/expected/rangetypes.out      |  54 ++++++++
 src/test/regress/sql/multirangetypes.sql      |  22 ++++
 src/test/regress/sql/rangetypes.sql           |  10 ++
 10 files changed, 395 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 16b94461b2c..0ca4ed6c2f8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -865,7 +865,7 @@
       </para>
       <para>
        The B-tree operator family this entry sorts according to, if an
-       ordering operator; zero if a search operator
+       ordering operator; zero if a search operator or portion operator
       </para></entry>
      </row>
     </tbody>
diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index f82e6f42d98..b705aee6be5 100644
--- a/src/backend/utils/adt/multirangetypes.c
+++ b/src/backend/utils/adt/multirangetypes.c
@@ -1226,6 +1226,55 @@ multirange_minus_internal(Oid mltrngtypoid, TypeCacheEntry *rangetyp,
 	return make_multirange(mltrngtypoid, rangetyp, range_count3, ranges3);
 }
 
+/*
+ * multirange minus but returning an array of one,
+ * or zero if the result would be empty.
+ */
+
+Datum
+multirange_without_portion(PG_FUNCTION_ARGS)
+{
+	MultirangeType *mr1 = PG_GETARG_MULTIRANGE_P(0);
+	MultirangeType *mr2 = PG_GETARG_MULTIRANGE_P(1);
+	Oid			mltrngtypoid = MultirangeTypeGetOid(mr1);
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *rangetyp;
+	int32		range_count1;
+	int32		range_count2;
+	RangeType **ranges1;
+	RangeType **ranges2;
+	MultirangeType *mr;
+	Datum	datums[1];
+	ArrayType *ret;
+
+	typcache = multirange_get_typcache(fcinfo, mltrngtypoid);
+	rangetyp = typcache->rngtype;
+
+	if (MultirangeIsEmpty(mr1) || MultirangeIsEmpty(mr2))
+		mr = mr1;
+	else
+	{
+		multirange_deserialize(typcache->rngtype, mr1, &range_count1, &ranges1);
+		multirange_deserialize(typcache->rngtype, mr2, &range_count2, &ranges2);
+
+		mr = multirange_minus_internal(mltrngtypoid,
+									   rangetyp,
+									   range_count1,
+									   ranges1,
+									   range_count2,
+									   ranges2);
+	}
+
+	if (MultirangeIsEmpty(mr))
+		PG_RETURN_ARRAYTYPE_P(construct_empty_array(typcache->type_id));
+	else
+	{
+		datums[0] = MultirangeTypePGetDatum(mr);
+		ret = construct_array(datums, 1, typcache->type_id, typcache->typlen, typcache->typbyval, typcache->typalign);
+		PG_RETURN_ARRAYTYPE_P(ret);
+	}
+}
+
 /* multirange intersection */
 Datum
 multirange_intersect(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index d99b00b5908..a0ec4682a5f 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -37,6 +37,7 @@
 #include "miscadmin.h"
 #include "nodes/miscnodes.h"
 #include "port/pg_bitutils.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/lsyscache.h"
@@ -1206,6 +1207,129 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 	return false;
 }
 
+/* subtraction but returning an array to accommodate splits */
+Datum
+range_without_portion(PG_FUNCTION_ARGS)
+{
+	RangeType  *r1 = PG_GETARG_RANGE_P(0);
+	RangeType  *r2 = PG_GETARG_RANGE_P(1);
+	RangeType  *rs[2];
+	int			n;
+	TypeCacheEntry *typcache;
+
+	/* Different types should be prevented by ANYRANGE matching rules */
+	if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2))
+		elog(ERROR, "range types do not match");
+
+	typcache = range_get_typcache(fcinfo, RangeTypeGetOid(r1));
+
+	range_without_portion_internal(typcache, r1, r2, rs, &n);
+
+	if (n == 0)
+		PG_RETURN_ARRAYTYPE_P(construct_empty_array(typcache->type_id));
+	else
+	{
+		Datum		xs[2];
+		int			i;
+		ArrayType	*ret;
+
+		for (i = 0; i < n; i++)
+			xs[i] = RangeTypePGetDatum(rs[i]);
+
+		ret = construct_array(xs, n, typcache->type_id, typcache->typlen, typcache->typbyval, typcache->typalign);
+		PG_RETURN_ARRAYTYPE_P(ret);
+	}
+}
+
+/*
+ * range_without_portion_internal - Sets outputs and outputn to the ranges
+ * remaining and their count (respectively) after subtracting r2 from r1.
+ * The array should never contain empty ranges.
+ * The outputs will be ordered. We expect that outputs is an array of
+ * RangeType pointers, already allocated with two slots.
+ */
+void
+range_without_portion_internal(TypeCacheEntry *typcache, RangeType *r1,
+							   RangeType *r2, RangeType **outputs, int *outputn)
+{
+	int			cmp_l1l2,
+				cmp_l1u2,
+				cmp_u1l2,
+				cmp_u1u2;
+	RangeBound	lower1,
+				lower2;
+	RangeBound	upper1,
+				upper2;
+	bool		empty1,
+				empty2;
+
+	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+	if (empty1)
+	{
+		/* if r1 is empty then r1 - r2 is empty, so return zero results */
+		*outputn = 0;
+		return;
+	}
+	else if (empty2)
+	{
+		/* r2 is empty so the result is just r1 (which we know is not empty) */
+		outputs[0] = r1;
+		*outputn = 1;
+		return;
+	}
+
+	/*
+	 * Use the same logic as range_minus_internal,
+	 * but support the split case
+	 */
+	cmp_l1l2 = range_cmp_bounds(typcache, &lower1, &lower2);
+	cmp_l1u2 = range_cmp_bounds(typcache, &lower1, &upper2);
+	cmp_u1l2 = range_cmp_bounds(typcache, &upper1, &lower2);
+	cmp_u1u2 = range_cmp_bounds(typcache, &upper1, &upper2);
+
+	if (cmp_l1l2 < 0 && cmp_u1u2 > 0)
+	{
+		lower2.inclusive = !lower2.inclusive;
+		lower2.lower = false;	/* it will become the upper bound */
+		outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL);
+
+		upper2.inclusive = !upper2.inclusive;
+		upper2.lower = true;	/* it will become the lower bound */
+		outputs[1] = make_range(typcache, &upper2, &upper1, false, NULL);
+
+		*outputn = 2;
+	}
+	else if (cmp_l1u2 > 0 || cmp_u1l2 < 0)
+	{
+		outputs[0] = r1;
+		*outputn = 1;
+	}
+	else if (cmp_l1l2 >= 0 && cmp_u1u2 <= 0)
+	{
+		*outputn = 0;
+	}
+	else if (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0)
+	{
+		lower2.inclusive = !lower2.inclusive;
+		lower2.lower = false;	/* it will become the upper bound */
+		outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL);
+		*outputn = 1;
+	}
+	else if (cmp_l1l2 >= 0 && cmp_u1u2 >= 0 && cmp_l1u2 <= 0)
+	{
+		upper2.inclusive = !upper2.inclusive;
+		upper2.lower = true;	/* it will become the lower bound */
+		outputs[0] = make_range(typcache, &upper2, &upper1, false, NULL);
+		*outputn = 1;
+	}
+	else
+	{
+		elog(ERROR, "unexpected case in range_without_portion");
+	}
+}
+
 /* range -> range aggregate functions */
 
 Datum
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 0e7511dde1c..d226c84aab3 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3457,5 +3457,11 @@
   oprresult => 'bool', oprcom => '<<(anymultirange,anymultirange)',
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
+{ oid => '8409', descr => 'range without portion',
+  oprname => '@-', oprleft => 'anyrange', oprright => 'anyrange',
+  oprresult => 'anyarray', oprcode => 'range_without_portion' },
+{ oid => '8407', descr => 'multirange without portion',
+  oprname => '@-', oprleft => 'anymultirange', oprright => 'anymultirange',
+  oprresult => 'anyarray', oprcode => 'multirange_without_portion' },
 
 ]
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 14200460abb..8f95e082170 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10570,6 +10570,9 @@
 { oid => '3869',
   proname => 'range_minus', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_minus' },
+{ oid => '8408',
+  proname => 'range_without_portion', prorettype => 'anyarray',
+  proargtypes => 'anyrange anyrange', prosrc => 'range_without_portion' },
 { oid => '3870', descr => 'less-equal-greater',
   proname => 'range_cmp', prorettype => 'int4',
   proargtypes => 'anyrange anyrange', prosrc => 'range_cmp' },
@@ -10857,6 +10860,9 @@
 { oid => '4271',
   proname => 'multirange_minus', prorettype => 'anymultirange',
   proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_minus' },
+{ oid => '8406',
+  proname => 'multirange_without_portion', prorettype => 'anyarray',
+  proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_without_portion' },
 { oid => '4272',
   proname => 'multirange_intersect', prorettype => 'anymultirange',
   proargtypes => 'anymultirange anymultirange',
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 2b574873cef..e1b256b65f4 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,5 +164,7 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache);
 extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1,
 								 const RangeType *r2, RangeType **output1,
 								 RangeType **output2);
+extern void range_without_portion_internal(TypeCacheEntry *typcache, RangeType *r1,
+									RangeType *r2, RangeType **outputs, int *outputn);
 
 #endif							/* RANGETYPES_H */
diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out
index 9808587532c..a41a865cc1d 100644
--- a/src/test/regress/expected/multirangetypes.out
+++ b/src/test/regress/expected/multirangetypes.out
@@ -2200,6 +2200,127 @@ SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0
  {[1,2),[4,5)}
 (1 row)
 
+-- without_portion
+SELECT nummultirange() @- nummultirange();
+ ?column? 
+----------
+ {}
+(1 row)
+
+SELECT nummultirange() @- nummultirange(numrange(1,2));
+ ?column? 
+----------
+ {}
+(1 row)
+
+SELECT nummultirange(numrange(1,2)) @- nummultirange();
+  ?column?   
+-------------
+ {"{[1,2)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(3,4)) @- nummultirange();
+     ?column?      
+-------------------
+ {"{[1,2),[3,4)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(1,2));
+ ?column? 
+----------
+ {}
+(1 row)
+
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(2,4));
+  ?column?   
+-------------
+ {"{[1,2)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(3,4));
+  ?column?   
+-------------
+ {"{[1,2)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(1,2));
+  ?column?   
+-------------
+ {"{[2,4)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(2,3));
+     ?column?      
+-------------------
+ {"{[1,2),[3,4)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(0,8));
+ ?column? 
+----------
+ {}
+(1 row)
+
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(0,2));
+  ?column?   
+-------------
+ {"{[2,4)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,8)) @- nummultirange(numrange(0,2), numrange(3,4));
+     ?column?      
+-------------------
+ {"{[2,3),[4,8)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,8)) @- nummultirange(numrange(2,3), numrange(5,null));
+     ?column?      
+-------------------
+ {"{[1,2),[3,5)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(-2,0));
+     ?column?      
+-------------------
+ {"{[1,2),[4,5)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(2,4));
+     ?column?      
+-------------------
+ {"{[1,2),[4,5)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(3,5));
+  ?column?   
+-------------
+ {"{[1,2)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(0,9));
+ ?column? 
+----------
+ {}
+(1 row)
+
+SELECT nummultirange(numrange(1,3), numrange(4,5)) @- nummultirange(numrange(2,9));
+  ?column?   
+-------------
+ {"{[1,2)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(8,9));
+     ?column?      
+-------------------
+ {"{[1,2),[4,5)}"}
+(1 row)
+
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(-2,0), numrange(8,9));
+     ?column?      
+-------------------
+ {"{[1,2),[4,5)}"}
+(1 row)
+
 -- intersection
 SELECT nummultirange() * nummultirange();
  ?column? 
diff --git a/src/test/regress/expected/rangetypes.out b/src/test/regress/expected/rangetypes.out
index 07d5621ef87..f536e8f7aaa 100644
--- a/src/test/regress/expected/rangetypes.out
+++ b/src/test/regress/expected/rangetypes.out
@@ -481,6 +481,60 @@ select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
  empty
 (1 row)
 
+select 'empty'::numrange @- numrange(2.0, 3.0);
+ ?column? 
+----------
+ {}
+(1 row)
+
+select numrange(1.1, 2.2) @- 'empty'::numrange;
+   ?column?    
+---------------
+ {"[1.1,2.2)"}
+(1 row)
+
+select numrange(1.1, 2.2) @- numrange(2.0, 3.0);
+   ?column?    
+---------------
+ {"[1.1,2.0)"}
+(1 row)
+
+select numrange(1.1, 2.2) @- numrange(2.2, 3.0);
+   ?column?    
+---------------
+ {"[1.1,2.2)"}
+(1 row)
+
+select numrange(1.1, 2.2,'[]') @- numrange(2.0, 3.0);
+   ?column?    
+---------------
+ {"[1.1,2.0)"}
+(1 row)
+
+select numrange(1.0, 3.0) @- numrange(1.5, 2.0);
+         ?column?          
+---------------------------
+ {"[1.0,1.5)","[2.0,3.0)"}
+(1 row)
+
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
+ range_without_portion 
+-----------------------
+ {"[10.1,12.2]"}
+(1 row)
+
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
+ range_without_portion 
+-----------------------
+ {}
+(1 row)
+
+select range_without_portion(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]'));
+   range_without_portion   
+---------------------------
+ {"[1.0,1.5]","(2.0,3.0]"}
+(1 row)
+
 select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5);
  ?column? 
 ----------
diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql
index cadf312031f..aa6f20d8adb 100644
--- a/src/test/regress/sql/multirangetypes.sql
+++ b/src/test/regress/sql/multirangetypes.sql
@@ -414,6 +414,28 @@ SELECT nummultirange(numrange(1,3), numrange(4,5)) - nummultirange(numrange(2,9)
 SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(8,9));
 SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0), numrange(8,9));
 
+-- without_portion
+SELECT nummultirange() @- nummultirange();
+SELECT nummultirange() @- nummultirange(numrange(1,2));
+SELECT nummultirange(numrange(1,2)) @- nummultirange();
+SELECT nummultirange(numrange(1,2), numrange(3,4)) @- nummultirange();
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(1,2));
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(2,4));
+SELECT nummultirange(numrange(1,2)) @- nummultirange(numrange(3,4));
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(1,2));
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(2,3));
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(0,8));
+SELECT nummultirange(numrange(1,4)) @- nummultirange(numrange(0,2));
+SELECT nummultirange(numrange(1,8)) @- nummultirange(numrange(0,2), numrange(3,4));
+SELECT nummultirange(numrange(1,8)) @- nummultirange(numrange(2,3), numrange(5,null));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(-2,0));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(2,4));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(3,5));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(0,9));
+SELECT nummultirange(numrange(1,3), numrange(4,5)) @- nummultirange(numrange(2,9));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(8,9));
+SELECT nummultirange(numrange(1,2), numrange(4,5)) @- nummultirange(numrange(-2,0), numrange(8,9));
+
 -- intersection
 SELECT nummultirange() * nummultirange();
 SELECT nummultirange() * nummultirange(numrange(1,2));
diff --git a/src/test/regress/sql/rangetypes.sql b/src/test/regress/sql/rangetypes.sql
index c5dbe0c04f1..64938ad082a 100644
--- a/src/test/regress/sql/rangetypes.sql
+++ b/src/test/regress/sql/rangetypes.sql
@@ -107,6 +107,16 @@ select numrange(1.1, 2.2,'[]') - numrange(2.0, 3.0);
 select range_minus(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
 select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
 
+select 'empty'::numrange @- numrange(2.0, 3.0);
+select numrange(1.1, 2.2) @- 'empty'::numrange;
+select numrange(1.1, 2.2) @- numrange(2.0, 3.0);
+select numrange(1.1, 2.2) @- numrange(2.2, 3.0);
+select numrange(1.1, 2.2,'[]') @- numrange(2.0, 3.0);
+select numrange(1.0, 3.0) @- numrange(1.5, 2.0);
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
+select range_without_portion(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]'));
+
 select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5);
 select numrange(1.0, 2.0) << numrange(3.0, 4.0);
 select numrange(1.0, 3.0,'[]') << numrange(3.0, 4.0,'[]');
-- 
2.42.0

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

- Added bison support for FOR PORTION OF syntax. The bounds must be
  constant, so we forbid column references, subqueries, etc. But we
  permit the magic UNBOUNDED keyword as the FROM or the TO (or both)
  to perform an unbounded update/delete. We also accept functions like
  NOW().
- Added logic to executor to insert new rows for the "leftover" part of
  a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
  trigger functions. Our triggers use this to learn what bounds were
  given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   5 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   4 +
 doc/src/sgml/catalogs.sgml                    |  19 +-
 doc/src/sgml/ref/delete.sgml                  |  69 +++
 doc/src/sgml/ref/update.sgml                  |  87 +++
 doc/src/sgml/trigger.sgml                     |  60 +-
 src/backend/access/gist/gistvalidate.c        |  39 +-
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/commands/trigger.c                |  49 ++
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/nodeModifyTable.c        | 260 +++++++-
 src/backend/nodes/nodeFuncs.c                 |  24 +
 src/backend/optimizer/plan/createplan.c       |   8 +-
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/optimizer/util/pathnode.c         |   4 +-
 src/backend/parser/analyze.c                  | 209 ++++++-
 src/backend/parser/gram.y                     |  56 +-
 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          |  40 ++
 src/backend/utils/adt/ri_triggers.c           |  74 +++
 src/backend/utils/cache/lsyscache.c           |  53 ++
 src/include/access/stratnum.h                 |   4 +-
 src/include/catalog/pg_amop.dat               |  15 +
 src/include/catalog/pg_amop.h                 |   1 +
 src/include/commands/trigger.h                |   1 +
 src/include/nodes/execnodes.h                 |  26 +
 src/include/nodes/parsenodes.h                |  46 +-
 src/include/nodes/pathnodes.h                 |   1 +
 src/include/nodes/plannodes.h                 |   1 +
 src/include/nodes/primnodes.h                 |  26 +
 src/include/optimizer/pathnode.h              |   3 +-
 src/include/parser/analyze.h                  |   3 +-
 src/include/parser/kwlist.h                   |   1 +
 src/include/parser/parse_node.h               |   1 +
 src/include/utils/lsyscache.h                 |   2 +
 src/test/regress/expected/for_portion_of.out  | 562 ++++++++++++++++++
 src/test/regress/expected/opr_sanity.out      |   6 +-
 src/test/regress/expected/privileges.out      |  18 +
 src/test/regress/expected/updatable_views.out |  32 +
 .../regress/expected/without_overlaps.out     | 144 ++++-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/for_portion_of.sql       | 423 +++++++++++++
 src/test/regress/sql/opr_sanity.sql           |   1 +
 src/test/regress/sql/privileges.sql           |  18 +
 src/test/regress/sql/updatable_views.sql      |  14 +
 src/test/regress/sql/without_overlaps.sql     |  93 ++-
 50 files changed, 2460 insertions(+), 71 deletions(-)
 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 d83f6ae8cbc..0058f6c7a6c 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -6293,6 +6293,11 @@ DELETE FROM ft2
 
 DELETE FROM ft2 WHERE ft2.c1 > 2000;
 ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw');
+-- Test UPDATE/DELETE with FOR PORTION OF
+UPDATE ft2 FOR PORTION OF c3 FROM 'a' TO 'b' SET c1 = 1;
+ERROR:  foreign tables don't support FOR PORTION OF
+DELETE FROM ft2 FOR PORTION OF c3 FROM 'a' TO 'b';
+ERROR:  foreign tables don't support FOR PORTION OF
 -- Test that trigger on remote table works as expected
 CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
 BEGIN
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 90c8fa4b705..6cb6f674ff3 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1546,6 +1546,10 @@ DELETE FROM ft2
 DELETE FROM ft2 WHERE ft2.c1 > 2000;
 ALTER SERVER loopback OPTIONS (ADD extensions 'postgres_fdw');
 
+-- Test UPDATE/DELETE with FOR PORTION OF
+UPDATE ft2 FOR PORTION OF c3 FROM 'a' TO 'b' SET c1 = 1;
+DELETE FROM ft2 FOR PORTION OF c3 FROM 'a' TO 'b';
+
 -- Test that trigger on remote table works as expected
 CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
 BEGIN
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0ca4ed6c2f8..1ac8b74fe3d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -757,12 +757,14 @@
    The catalog <structname>pg_amop</structname> stores information about
    operators associated with access method operator families.  There is one
    row for each operator that is a member of an operator family.  A family
-   member can be either a <firstterm>search</firstterm> operator or an
-   <firstterm>ordering</firstterm> operator.  An operator
-   can appear in more than one family, but cannot appear in more than one
-   search position nor more than one ordering position within a family.
-   (It is allowed, though unlikely, for an operator to be used for both
-   search and ordering purposes.)
+   member can be either a <firstterm>search</firstterm> operator, an
+   <firstterm>ordering</firstterm> operator, or a
+   <firstterm>portion</firstterm> operator. A
+   <firstterm>portion</firstterm> operator is used for computing
+   <literal>FOR PORTION OF</literal> results. An operator can appear in more
+   than one family, but cannot appear in more than one search, ordering, or
+   portion position within a family. (It is allowed, though unlikely, for an
+   operator to be used for more than one purpose.)
   </para>
 
   <table>
@@ -833,8 +835,9 @@
        <structfield>amoppurpose</structfield> <type>char</type>
       </para>
       <para>
-       Operator purpose, either <literal>s</literal> for search or
-       <literal>o</literal> for ordering
+       Operator purpose, either <literal>s</literal> for search,
+       <literal>o</literal> for ordering,
+       or <literal>p</literal> for portion
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 1b81b4e7d74..1c7275341ea 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
 DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+    [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
     [ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
     [ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
@@ -54,6 +55,39 @@ 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 your delete will
+   only affect rows that overlap the given interval. Furthermore, if a row's span
+   extends outside the <literal>FOR PORTION OF</literal> bounds, then your delete
+   will only change the span within those bounds. In effect you are deleting any
+   moment targeted by <literal>FOR PORTION OF</literal> and no moments outside.
+  </para>
+
+  <para>
+   Specifically, after <productname>PostgreSQL</productname> deletes the existing row,
+   it will <literal>INSERT</literal>
+   new rows whose range or start/end column(s) receive the remaining span outside
+   the targeted bounds, containing the original values in other columns.
+   There will be zero to two inserted records,
+   depending on whether the original span extended before the targeted
+   <literal>FROM</literal>, after the targeted <literal>TO</literal>, both, or neither.
+  </para>
+
+  <para>
+   These secondary inserts fire <literal>INSERT</literal> triggers. First
+   <literal>BEFORE DELETE</literal> triggers 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.
@@ -116,6 +150,41 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+    <listitem>
+     <para>
+      The range column or period to use when performing a temporal delete.
+     </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 or period from
+      <replaceable class="parameter">range_or_period_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 or period from
+      <replaceable class="parameter">range_or_period_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>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 2ab24b0523e..71a02697f2d 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
 UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+    [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> <replaceable class="parameter">for_portion_of_target</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> )
@@ -51,6 +52,41 @@ 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 your update will
+   only affect rows that overlap the given interval. Furthermore, if a row's span
+   extends outside the <literal>FOR PORTION OF</literal> bounds, then your update
+   will only change the span within those bounds. In effect you are updating any
+   moment targeted by <literal>FOR PORTION OF</literal> and no moments outside.
+  </para>
+
+  <para>
+   Specifically, when <productname>PostgreSQL</productname> updates the existing row,
+   it will also change the range or start/end column(s) so that their interval
+   no longer extends beyond the targeted <literal>FOR PORTION OF</literal> bounds.
+   Then <productname>PostgreSQL</productname> will <literal>INSERT</literal>
+   new rows whose range or start/end column(s) receive the remaining span outside
+   the targeted bounds, containing the un-updated values in other columns.
+   There will be zero to two inserted records,
+   depending on whether the original span extended before the targeted
+   <literal>FROM</literal>, after the targeted <literal>TO</literal>, both, or neither.
+  </para>
+
+  <para>
+   These secondary inserts fire <literal>INSERT</literal> triggers. First
+   <literal>BEFORE UPDATE</literal> triggers 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.
@@ -114,6 +150,57 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+    <listitem>
+     <para>
+      The range column or period to use when performing a temporal update.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">for_portion_of_target</replaceable></term>
+    <listitem>
+     <para>
+      The interval to update. If you are targeting a range column or <literal>PERIOD</literal>,
+      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">expression</replaceable><literal>)</literal>
+      where the expression yields a value for the same type as
+      <replaceable class="parameter">range_or_period_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 or period from
+      <replaceable class="parameter">range_or_period_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 or period from
+      <replaceable class="parameter">range_or_period_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>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index a5390ff6443..538a1e078dc 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -535,17 +535,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>
 
@@ -813,6 +814,43 @@ 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/PERIOD named in FOR PORTION OF */
+    Oid     fp_rangeType;       /* the type of the FOR PORTION OF expression */
+    bool    fp_hasPeriod;       /* true iff this is a PERIOD not a range */
+    char   *fp_periodStartName; /* the PERIOD's start column */
+    char   *fp_periodEndName;   /* the PERIOD's end column */
+    Datum   fp_targetRange;     /* the range from FOR PORTION OF */
+} ForPortionOfState;
+</programlisting>
+
+       where <structfield>fp_rangeName</structfield> is the period or range
+       column named in the <literal>FOR PORTION OF</literal> clause,
+       <structfield>fp_rangeType</structfield> is its range type,
+       <structfield>fp_hasPeriod</structfield> indicates whether a period was used
+       or a range column, <structfield>fp_periodStartName</structfield> and
+       <structfield>fp_periodEndName</structfield> are the names of the period's
+       start and end columns (or <symbol>NULL</symbol> if a range column was used),
+       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/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 4bd2e531d71..97c139a10d3 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -199,7 +199,7 @@ gistvalidate(Oid opclassoid)
 		}
 
 		/* GiST supports ORDER BY operators */
-		if (oprform->amoppurpose != AMOP_SEARCH)
+		if (oprform->amoppurpose == AMOP_ORDER)
 		{
 			/* ... but must have matching distance proc */
 			if (!OidIsValid(get_opfamily_proc(opfamilyoid,
@@ -226,24 +226,43 @@ gistvalidate(Oid opclassoid)
 				result = false;
 			}
 		}
-		else
+		else if (oprform->amoppurpose == AMOP_SEARCH)
 		{
 			/* Search operators must always return bool */
 			op_rettype = BOOLOID;
 		}
-
-		/* Check operator signature */
-		if (!check_amop_signature(oprform->amopopr, op_rettype,
-								  oprform->amoplefttype,
-								  oprform->amoprighttype))
+		else if (oprform->amoppurpose == AMOP_PORTION)
 		{
+			/* No restrictions on PORTION */
+			op_rettype = InvalidOid;
+		}
+		else
+		{
+			/* Nothing else it could be */
 			ereport(INFO,
 					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-					 errmsg("operator family \"%s\" of access method %s contains operator %s with wrong signature",
-							opfamilyname, "gist",
-							format_operator(oprform->amopopr))));
+					 errmsg("operator family \"%s\" of access method %s contains unknown purpose %c for operator %s",
+						 opfamilyname, "gist", oprform->amoppurpose,
+						 format_operator(oprform->amopopr))));
+			op_rettype = InvalidOid;
 			result = false;
 		}
+
+		/* Check operator signature */
+		if (OidIsValid(op_rettype))
+		{
+			if (!check_amop_signature(oprform->amopopr, op_rettype,
+									  oprform->amoplefttype,
+									  oprform->amoprighttype))
+			{
+				ereport(INFO,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("operator family \"%s\" of access method %s contains operator %s with wrong signature",
+								opfamilyname, "gist",
+								format_operator(oprform->amopopr))));
+				result = false;
+			}
+		}
 	}
 
 	/* Now check for inconsistent groups of operators/functions */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cdef174ee24..ef7f58f99ac 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12609,6 +12609,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;
 
 		fcinfo->context = (Node *) &trigdata;
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index a7c92416f56..a0db2f01323 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -54,6 +54,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/bytea.h"
+#include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/inval.h"
@@ -2638,6 +2639,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];
@@ -2737,6 +2739,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;
@@ -2828,6 +2831,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);
 
@@ -2891,6 +2895,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];
@@ -3026,6 +3031,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++)
@@ -3175,6 +3181,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);
 
@@ -3641,6 +3648,7 @@ typedef struct AfterTriggerSharedData
 	Oid			ats_tgoid;		/* the trigger's ID */
 	Oid			ats_relid;		/* the relation it's on */
 	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;
@@ -3914,6 +3922,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);

@@ -4123,6 +4132,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
 			newshared->ats_relid == evtshared->ats_relid &&
 			newshared->ats_event == evtshared->ats_event &&
 			newshared->ats_table == evtshared->ats_table &&
+			newshared->for_portion_of == evtshared->for_portion_of &&
 			newshared->ats_firing_id == 0)
 			break;
 	}
@@ -4487,6 +4497,7 @@ AfterTriggerExecute(EState *estate,
 	LocTriggerData.tg_relation = rel;
 	if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
 		LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
+	LocTriggerData.tg_temporal = evtshared->for_portion_of;
 
 	MemoryContextReset(per_tuple_context);
 
@@ -6023,6 +6034,43 @@ 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_hasPeriod = src->fp_hasPeriod;
+		dst->fp_rangeAttno = src->fp_rangeAttno;
+		dst->fp_periodStartAttno = src->fp_periodStartAttno;
+		dst->fp_periodEndAttno = src->fp_periodEndAttno;
+
+		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()
  *
@@ -6438,6 +6486,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 		else
 			new_shared.ats_table = NULL;
 		new_shared.ats_modifiedcols = afterTriggerCopyBitmap(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/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 13a9b7da83b..b2eb5bde736 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1274,6 +1274,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 9fc5abff8f0..dfdcdbcde6d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -58,9 +58,11 @@
 #include "rewrite/rewriteHandler.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/memutils.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"

@@ -141,6 +143,10 @@ static bool ExecOnConflictUpdate(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,
@@ -1205,6 +1211,130 @@ 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;
+	Datum	allLeftovers;
+	Datum	*leftovers;
+	int		nleftovers;
+	bool isNull = false;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+	TupleTableSlot *leftoverSlot = fpoState->fp_Leftover;
+	TupleConversionMap *map = NULL;
+	HeapTuple oldtuple = NULL;
+
+	/*
+	 * Get the range of the old pre-UPDATE/DELETE tuple,
+	 * so we can intersect it with the FOR PORTION OF target
+	 * and see if there are any "leftovers" to insert.
+	 *
+	 * We have already locked the tuple in ExecUpdate/ExecDelete
+	 * and it has passed EvalPlanQual.
+	 * Make sure we're looking at the most recent version.
+	 * Otherwise concurrent updates of the same tuple in READ COMMITTED
+	 * could insert conflicting "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.
+	 * 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];
+	oldRange = slot_getattr(oldtupleSlot, rangeAttno, &isNull);
+
+	if (isNull)
+		elog(ERROR, "found a NULL range in a temporal table");
+
+	/*
+	 * Get the range's type cache entry. This is worth caching for the whole UPDATE
+	 * 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. */
+
+	allLeftovers = OidFunctionCall2Coll(forPortionOf->withoutPortionProc,
+										InvalidOid, oldRange, fpoState->fp_targetRange);
+
+	deconstruct_array(DatumGetArrayTypeP(allLeftovers), typcache->type_id, typcache->typlen,
+					  typcache->typbyval, typcache->typalign, &leftovers, NULL, &nleftovers);
+
+	Assert(nleftovers >= 0);
+	if (nleftovers > 0)
+	{
+		bool shouldFree = false;
+		int i;
+
+		/*
+		 * 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 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);
+		}
+
+		for (i = 0; i < nleftovers; i++)
+		{
+			/* store the new range */
+			leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftovers[i];
+			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;
+			// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+			ExecInsert(context, resultRelInfo, leftoverSlot, node->canSetTag, NULL, NULL);
+		}
+
+		if (shouldFree)
+			heap_freetuple(oldtuple);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1357,7 +1487,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 leftovers from a FOR PORTION OF
+ * delete.
  */
 static void
 ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1390,6 +1521,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
+	/* Compute leftovers in FOR PORTION OF */
+	// TODO: Skip this for FDW deletes?
+	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);
@@ -1770,7 +1906,11 @@ 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;
@@ -2139,6 +2279,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL, NIL,
 											   (updateCxt->updateIndexes == TU_Summarizing));
 
+	/* Compute leftovers in FOR PORTION OF */
+	// TODO: Skip this for FDW updates?
+	if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf)
+		ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid);
+
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(context->estate, resultRelInfo,
 						 NULL, NULL,
@@ -4303,6 +4448,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+	 * If needed, initialize the target range for FOR PORTION OF.
+	 */
+	if (node->forPortionOf)
+	{
+		TupleDesc tupDesc;
+		ForPortionOfExpr *forPortionOf;
+		Datum	targetRange;
+		bool	isNull;
+		ExprContext *econtext;
+		ExprState *exprState;
+		ForPortionOfState *fpoState;
+
+		resultRelInfo = mtstate->resultRelInfo;
+		if (resultRelInfo->ri_RootResultRelInfo)
+			resultRelInfo = resultRelInfo->ri_RootResultRelInfo;
+
+		tupDesc = resultRelInfo->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 range");
+
+		/* 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(resultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/* Create the tuple slot for INSERTing the leftovers */
+
+		fpoState->fp_Leftover =
+			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+
+		/*
+		 * We must attach the ForPortionOfState to all result rels,
+		 * in case of a cross-partition update or triggers firing
+		 * on partitions.
+		 */
+		for (i = 0; i < nrels; i++)
+		{
+			ForPortionOfState	*leafState;
+			resultRelInfo = &mtstate->resultRelInfo[i];
+
+			leafState = makeNode(ForPortionOfState);
+			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;
+		}
+
+		/* Make sure the root relation has the FOR PORTION OF clause too. */
+		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 leftovers.
+			 * Since we are initializing things before ExecCrossPartitionUpdate runs,
+			 * we must do everything it needs as well.
+			 */
+			if (mtstate->mt_partition_tuple_routing == NULL)
+			{
+				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 e1a5bc7e95d..6c19844e17e 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2444,6 +2444,14 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_ForPortionOfExpr:
+			{
+				ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node;
+
+				if (WALK(forPortionOf->targetRange))
+					return true;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -2584,6 +2592,8 @@ query_tree_walker_impl(Query *query,
 		return true;
 	if (WALK(query->mergeActionList))
 		return true;
+	if (WALK(query->forPortionOf))
+		return true;
 	if (WALK(query->returningList))
 		return true;
 	if (WALK(query->jointree))
@@ -3430,6 +3440,19 @@ 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->targetRange, fpo->targetRange, Node *);
+				MUTATE(newnode->rangeTargetList, fpo->rangeTargetList, List *);
+
+				return (Node *) newnode;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3608,6 +3631,7 @@ query_tree_mutator_impl(Query *query,
 	MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
 	MUTATE(query->onConflict, query->onConflict, OnConflictExpr *);
 	MUTATE(query->mergeActionList, query->mergeActionList, List *);
+	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 ca619eab944..41f5f8d2f7b 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -311,7 +311,8 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 List *updateColnosLists,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict,
-									 List *mergeActionLists, int epqParam);
+									 List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+									 int epqParam);
 static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
 											 GatherMergePath *best_path);
 
@@ -2835,6 +2836,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->rowMarks,
 							best_path->onconflict,
 							best_path->mergeActionLists,
+							best_path->forPortionOf,
 							best_path->epqParam);
 
 	copy_generic_path_info(&plan->plan, &best_path->path);
@@ -7005,7 +7007,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 				 List *updateColnosLists,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict,
-				 List *mergeActionLists, int epqParam)
+				 List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+				 int epqParam)
 {
 	ModifyTable *node = makeNode(ModifyTable);
 	List	   *fdw_private_list;
@@ -7071,6 +7074,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 		node->exclRelTlist = onconflict->exclRelTlist;
 	}
 	node->updateColnosLists = updateColnosLists;
+	node->forPortionOf = (Node *) forPortionOf;
 	node->withCheckOptionLists = withCheckOptionLists;
 	node->returningLists = returningLists;
 	node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 667723b6753..0443793d3ac 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1963,6 +1963,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
 										rowMarks,
 										parse->onConflict,
 										mergeActionLists,
+										parse->forPortionOf,
 										assign_special_exec_param(root));
 		}
 
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8dbf790e893..22bde21a362 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3708,7 +3708,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 						List *updateColnosLists,
 						List *withCheckOptionLists, List *returningLists,
 						List *rowMarks, OnConflictExpr *onconflict,
-						List *mergeActionLists, int epqParam)
+						List *mergeActionLists, ForPortionOfExpr *forPortionOf,
+						int epqParam)
 {
 	ModifyTablePath *pathnode = makeNode(ModifyTablePath);
 
@@ -3774,6 +3775,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;
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 06fc8ce98b5..e786621100e 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,7 +24,9 @@
 
 #include "postgres.h"
 
+#include "access/stratnum.h"
 #include "access/sysattr.h"
+#include "catalog/pg_am.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
@@ -47,11 +49,13 @@
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
+#include "parser/parser.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/backend_status.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/syscache.h"
 
@@ -60,10 +64,16 @@
 post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
 
 static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree);
+static Node *addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf,
+											Node *whereClause);
 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);
 static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt);
@@ -479,6 +489,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
+addForPortionOfWhereConditions(Query *qry, ForPortionOfClause *forPortionOf, Node *whereClause)
+{
+	if (forPortionOf)
+	{
+		if (whereClause)
+			return (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, whereClause), -1);
+		else
+			return qry->forPortionOf->overlapsExpr;
+	}
+	else
+		return whereClause;
+}
+
 /*
  * analyze_requires_snapshot
  *		Returns true if a snapshot must be set before doing parse analysis
@@ -512,6 +536,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
 	qry->commandType = CMD_DELETE;
@@ -550,7 +575,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
 
-	qual = transformWhereClause(pstate, stmt->whereClause,
+	if (stmt->forPortionOf)
+		qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf, false);
+
+	whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+	qual = transformWhereClause(pstate, whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
 	qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -1173,7 +1202,7 @@ transformOnConflictClause(ParseState *pstate,
 		 * Now transform the UPDATE subexpressions.
 		 */
 		onConflictSet =
-			transformUpdateTargetList(pstate, onConflictClause->targetList);
+			transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1232,155 @@ 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.
+ *	  - 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;
+	RTEPermissionInfo *target_perminfo = pstate->p_target_nsitem->p_perminfo;
+	char *range_name = forPortionOf->range_name;
+	char *range_type_namespace = NULL;
+	char *range_type_name = NULL;
+	int range_attno = InvalidAttrNumber;
+	Form_pg_attribute attr;
+	Oid	opclass;
+	StrategyNumber strat;
+	Oid	opid;
+	ForPortionOfExpr *result;
+	Var *rangeVar;
+	Node *targetExpr;
+
+	/* 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);
+
+	/* Make sure it's a range column */
+	if (!type_is_range(attr->atttypid))
+		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)));
+
+	rangeVar = makeVar(
+			rtindex,
+			range_attno,
+			attr->atttypid,
+			attr->atttypmod,
+			attr->attcollation,
+			0);
+	rangeVar->location = forPortionOf->location;
+	result->rangeVar = rangeVar;
+	result->rangeType = attr->atttypid;
+	if (!get_typname_and_namespace(attr->atttypid, &range_type_name, &range_type_namespace))
+		elog(ERROR, "missing range type %d", attr->atttypid);
+
+
+	if (forPortionOf->target)
+		/*
+		 * We were already given an expression for the target,
+		 * so we don't have to build anything.
+		 */
+		targetExpr = forPortionOf->target;
+	else
+	{
+		/*
+		 * 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.
+		 *
+		 * It also permits UNBOUNDED in either place.
+		 */
+		targetExpr = (Node *) makeFuncCall(
+				list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+				list_make2(forPortionOf->target_start, forPortionOf->target_end),
+				COERCE_EXPLICIT_CALL,
+				forPortionOf->location);
+	}
+	result->targetRange = transformExpr(pstate, targetExpr, EXPR_KIND_UPDATE_PORTION);
+
+	/*
+	 * Build overlapsExpr to use in the whereClause.
+	 * 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);
+	strat = RTOverlapStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "overlaps", "FOR PORTION OF", &opid, &strat);
+	result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
+			(Node *) copyObject(rangeVar), targetExpr,
+			forPortionOf->location);
+
+	/*
+	 * Look up the withoutPortionOper so we can compute the leftovers.
+	 * Leftovers will be old_range @- target_range
+	 * (one per element of the result).
+	 */
+	strat = RTWithoutPortionStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "without portion", "FOR PORTION OF", &opid, &strat);
+	result->withoutPortionProc = get_opcode(opid);
+
+	if (isUpdate)
+	{
+		/*
+		 * Now make sure we update the start/end time of the record.
+		 * For a range col (r) this is `r = r * targetRange`.
+		 */
+		Expr *rangeTLEExpr;
+		TargetEntry *tle;
+
+		strat = RTIntersectStrategyNumber;
+		GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "intersects", "FOR PORTION OF", &opid, &strat);
+		rangeTLEExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
+				(Node *) copyObject(rangeVar), targetExpr,
+				forPortionOf->location);
+		rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
+
+		/* Make a TLE to set the range column */
+		result->rangeTargetList = NIL;
+		tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
+		result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+		/* Mark the range column as requiring update permissions */
+		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+													  range_attno - FirstLowInvalidHeapAttributeNumber);
+	}
+	else
+		result->rangeTargetList = NIL;
+
+	result->range_name = range_name;
+
+	return result;
+}
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2587,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
 	qry->commandType = CMD_UPDATE;
@@ -2426,6 +2605,10 @@ 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 */
@@ -2442,7 +2625,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;
 
-	qual = transformWhereClause(pstate, stmt->whereClause,
+	whereClause = addForPortionOfWhereConditions(qry, stmt->forPortionOf, stmt->whereClause);
+	qual = transformWhereClause(pstate, whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
 	qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -2451,7 +2635,7 @@ 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;
@@ -2470,7 +2654,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;
@@ -2520,6 +2704,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 							RelationGetRelationName(pstate->p_target_relation)),
 					 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("can't directly assign to \"%s\" in a FOR PORTION OF update",
+								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 a1182042b86..c85b8b094ec 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -258,6 +258,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	RangeVar   *range;
 	IntoClause *into;
 	WithClause *with;
+	ForPortionOfClause *forportionof;
 	InferClause	*infer;
 	OnConflictClause *onconflict;
 	A_Indices  *aind;
@@ -551,6 +552,7 @@ 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 <forportionof> for_portion_of_clause
 %type <node>	tablesample_clause opt_repeatable_clause
 %type <target>	target_el set_target insert_column_item
 
@@ -747,7 +749,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
 	PERIOD PLACING PLANS POLICY
-	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
+	PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
 	QUOTE
@@ -865,6 +867,17 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * json_predicate_type_constraint and json_key_uniqueness_constraint_opt
  * productions (see comments there).
  */
+/*
+ * We need to handle this shift/reduce conflict:
+ * FOR PORTION OF valid_at FROM INTERVAL YEAR TO MONTH TO foo.
+ * This is basically the classic "dangling else" problem, and we want a
+ * similar resolution: treat the TO as part of the INTERVAL, not as part of
+ * the FROM ... TO .... Users can add parentheses if that's a problem.
+ * TO just needs to be higher precedence than YEAR_P etc.
+ * TODO: I need to figure out a %prec solution before this gets committed!
+ */
+%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P
+%nonassoc TO
 %nonassoc	UNBOUNDED		/* 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
@@ -12231,14 +12244,16 @@ returning_clause:
  *****************************************************************************/
 
 DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+			for_portion_of_clause
 			using_clause where_or_current_clause returning_clause
 				{
 					DeleteStmt *n = makeNode(DeleteStmt);
 
 					n->relation = $4;
-					n->usingClause = $5;
-					n->whereClause = $6;
-					n->returningList = $7;
+					n->forPortionOf = $5;
+					n->usingClause = $6;
+					n->whereClause = $7;
+					n->returningList = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
@@ -12301,6 +12316,7 @@ opt_nowait_or_skip:
  *****************************************************************************/
 
 UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
+			for_portion_of_clause
 			SET set_clause_list
 			from_clause
 			where_or_current_clause
@@ -12309,10 +12325,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					UpdateStmt *n = makeNode(UpdateStmt);
 
 					n->relation = $3;
-					n->targetList = $5;
-					n->fromClause = $6;
-					n->whereClause = $7;
-					n->returningList = $8;
+					n->forPortionOf = $4;
+					n->targetList = $6;
+					n->fromClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
@@ -13748,6 +13765,27 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+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;
+				}
+			| 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;
+				}
+			| /*EMPTY*/					{ $$ = NULL; }
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -17341,6 +17379,7 @@ unreserved_keyword:
 			| PASSWORD
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -17945,6 +17984,7 @@ bare_label_keyword:
 			| PLACING
 			| PLANS
 			| POLICY
+			| PORTION
 			| POSITION
 			| PRECEDING
 			| PREPARE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 7b211a7743f..15b5c0dedc1 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -574,6 +574,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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
@@ -964,6 +971,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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 44529bb49e6..14ffb7ba649 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 9300c7b9abc..f3c49de8099 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -566,6 +566,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_PARTITION_BOUND:
 			err = _("cannot use column reference in partition bound expression");
 			break;
+		case EXPR_KIND_UPDATE_PORTION:
+			err = _("cannot use column reference in FOR PORTION OF expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1813,6 +1816,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_GENERATED_COLUMN:
 			err = _("cannot use subquery in column generation expression");
 			break;
+		case EXPR_KIND_UPDATE_PORTION:
+			err = _("cannot use subquery in FOR PORTION OF expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3104,6 +3110,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "UPDATE";
 		case EXPR_KIND_MERGE_WHEN:
 			return "MERGE WHEN";
+		case EXPR_KIND_UPDATE_PORTION:
+			return "FOR PORTION OF";
 		case EXPR_KIND_GROUP_BY:
 			return "GROUP BY";
 		case EXPR_KIND_ORDER_BY:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index fdb3e6df338..fc17dd7edf8 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2656,6 +2656,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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 5f6a683ab94..0733616f036 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -372,7 +372,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 					pstate->p_is_insert = false;
 					action->targetList =
 						transformUpdateTargetList(pstate,
-												  mergeWhenClause->targetList);
+												  mergeWhenClause->targetList, NULL);
 				}
 				break;
 			case CMD_DELETE:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f60b34deb64..4c4a5dd0727 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3476,6 +3476,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, pull up any WHERE quals from the view.  We know that
 	 * any Vars in the quals must reference the one base relation, so we need
@@ -3814,6 +3838,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 		else if (event == CMD_UPDATE)
 		{
 			Assert(parsetree->override == OVERRIDING_NOT_SET);
+			/*
+			 * Update FOR PORTION OF column(s) automatically. Don't
+			 * do this until we're done rewriting a view update, so
+			 * that we don't add the same update on the recursion.
+			 */
+			if (parsetree->forPortionOf &&
+				rt_entry_relation->rd_rel->relkind != RELKIND_VIEW)
+			{
+				ListCell *tl;
+				foreach(tl, parsetree->forPortionOf->rangeTargetList)
+				{
+					TargetEntry *tle = (TargetEntry *) lfirst(tl);
+					parsetree->targetList = lappend(parsetree->targetList, tle);
+				}
+			}
+
 			parsetree->targetList =
 				rewriteTargetListIU(parsetree->targetList,
 									parsetree->commandType,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 0dadf2acb2d..4d17b3877bd 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -230,6 +230,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 forPortionOfParam, Datum forPortionOf,
 							bool detectNewRows, int expect_OK);
 static void ri_ExtractValues(Relation rel, TupleTableSlot *slot,
 							 const RI_ConstraintInfo *riinfo, bool rel_is_pk,
@@ -240,6 +241,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 							   int queryno, bool partgone) pg_attribute_noreturn();
 static void lookupTRIOperAndProc(const RI_ConstraintInfo *riinfo,
 								 char **opname, char **aggname);
+static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal,
+								 const RI_ConstraintInfo *riinfo);
+static Datum restrict_cascading_range(const ForPortionOfState *tg_temporal,
+									  const RI_ConstraintInfo *riinfo,
+									  TupleTableSlot *oldslot);

 /*
@@ -456,6 +462,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -620,6 +627,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,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -719,6 +727,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
 
 	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
 									trigdata->tg_relation, true);
@@ -815,9 +825,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	/*
 	 * We have a plan now. Run it to check for existing references.
 	 */
+	if (trigdata->tg_temporal)
+	{
+		targetRangeParam = riinfo->nkeys - 1;
+		targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+	}
+
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					targetRangeParam, targetRange,
 					true,		/* must detect new rows */
 					SPI_OK_SELECT);
 
@@ -924,6 +941,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1045,6 +1063,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1277,6 +1296,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,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2551,6 +2571,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 				RI_QueryKey *qkey, SPIPlanPtr qplan,
 				Relation fk_rel, Relation pk_rel,
 				TupleTableSlot *oldslot, TupleTableSlot *newslot,
+				int forPortionOfParam, Datum forPortionOf,
 				bool detectNewRows, int expect_OK)
 {
 	Relation	query_rel,
@@ -2606,6 +2627,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 		ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
 						 vals, nulls);
 	}
+	/* Add one more query param if we are using FOR PORTION OF */
+	if (forPortionOf)
+	{
+		vals[forPortionOfParam] = forPortionOf;
+		nulls[forPortionOfParam] = ' ';
+	}
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3350,3 +3377,50 @@ lookupTRIOperAndProc(const RI_ConstraintInfo *riinfo, char **opname, char **aggn
 				 errhint("You must use an operator class with a referencedagg support function.")));
 	*aggname = get_func_name_and_namespace(oid);
 }
+
+/*
+ * 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_cascading_range -
+ *
+ * Returns a Datum of RangeTypeP holding the appropriate timespan
+ * to target child records when we CASCADE/SET NULL/SET DEFAULT.
+ *
+ * In a normal UPDATE/DELETE this should be the parent's own valid time,
+ * but if there was a FOR PORTION OF clause, then we should use that to
+ * trim down the parent's span further.
+ */
+static Datum
+restrict_cascading_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+{
+	Datum	pkRecordRange;
+	bool	isnull;
+
+	pkRecordRange = slot_getattr(oldslot, riinfo->pk_attnums[riinfo->nkeys - 1], &isnull);
+	if (isnull)
+		elog(ERROR, "application time should not be null");
+
+	if (fpo_targets_pk_range(tg_temporal, riinfo))
+	{
+		RangeType *r1 = DatumGetRangeTypeP(pkRecordRange);
+		RangeType *r2 = DatumGetRangeTypeP(tg_temporal->fp_targetRange);
+		Oid rngtypid = RangeTypeGetOid(r1);
+		TypeCacheEntry *typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+		return RangeTypePGetDatum(range_intersect_internal(typcache, r1, r2));
+	}
+	else
+		return pkRecordRange;
+}
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 6cd14d4bd12..9a24882f895 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2174,6 +2174,59 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
+ * get_typname
+ *
+ *	  Returns the name of a given type
+ *
+ * Returns a palloc'd copy of the string, or NULL if no such type.
+ */
+char *
+get_typname(Oid typid)
+{
+	HeapTuple   tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		char	   *result;
+
+		result = pstrdup(NameStr(typtup->typname));
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return NULL;
+}
+
+/*
+ * get_typname_and_namespace
+ *
+ *	  Returns the name and namespace of a given type
+ *
+ * Returns true if one found, or false if not.
+ */
+bool
+get_typname_and_namespace(Oid typid, char **typname, char **typnamespace)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		*typname = pstrdup(NameStr(typtup->typname));
+		*typnamespace = get_namespace_name(typtup->typnamespace);
+		ReleaseSysCache(tp);
+		/* *typnamespace is NULL if it wasn't found: */
+		return *typnamespace;
+	}
+	else
+		return false;
+}
+
 /*
  * get_typlen
  *
diff --git a/src/include/access/stratnum.h b/src/include/access/stratnum.h
index 8a47d3c9ec8..e93c4d1f86c 100644
--- a/src/include/access/stratnum.h
+++ b/src/include/access/stratnum.h
@@ -78,8 +78,10 @@ typedef uint16 StrategyNumber;
 #define RTPrefixStrategyNumber			28	/* for text ^@ */
 #define RTOldBelowStrategyNumber		29	/* for old spelling of <<| */
 #define RTOldAboveStrategyNumber		30	/* for old spelling of |>> */
+#define RTIntersectStrategyNumber		31	/* for * */
+#define RTWithoutPortionStrategyNumber	32	/* for @- */
 
-#define RTMaxStrategyNumber				30
+#define RTMaxStrategyNumber				32

 #endif							/* STRATNUM_H */
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index d8a05214b11..a565d337dec 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1108,6 +1108,9 @@
 { amopfamily => 'gist/box_ops', amoplefttype => 'box', amoprighttype => 'point',
   amopstrategy => '15', amoppurpose => 'o', amopopr => '<->(box,point)',
   amopmethod => 'gist', amopsortfamily => 'btree/float_ops' },
+{ amopfamily => 'gist/box_ops', amoplefttype => 'box', amoprighttype => 'box',
+  amopstrategy => '31', amoppurpose => 'p', amopopr => '#(box,box)',
+  amopmethod => 'gist' },
 
 # gist point_ops
 { amopfamily => 'gist/point_ops', amoplefttype => 'point',
@@ -1397,6 +1400,12 @@
 { amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '18',
   amopopr => '=(anyrange,anyrange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
+  amoprighttype => 'anyrange', amopstrategy => '31', amoppurpose => 'p',
+  amopopr => '*(anyrange,anyrange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
+  amoprighttype => 'anyrange', amopstrategy => '32', amoppurpose => 'p',
+  amopopr => '@-(anyrange,anyrange)', amopmethod => 'gist' },
 
 # GiST multirange_ops
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
@@ -1453,6 +1462,12 @@
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
   amoprighttype => 'anymultirange', amopstrategy => '18',
   amopopr => '=(anymultirange,anymultirange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
+  amoprighttype => 'anymultirange', amopstrategy => '31', amoppurpose => 'p',
+  amopopr => '*(anymultirange,anymultirange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
+  amoprighttype => 'anymultirange', amopstrategy => '32', amoppurpose => 'p',
+  amopopr => '@-(anymultirange,anymultirange)', amopmethod => 'gist' },
 
 # btree multirange_ops
 { amopfamily => 'btree/multirange_ops', amoplefttype => 'anymultirange',
diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h
index 8ea18a2fe1b..f561a11f7fb 100644
--- a/src/include/catalog/pg_amop.h
+++ b/src/include/catalog/pg_amop.h
@@ -96,6 +96,7 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_amop_oid_index, 2756, AccessMethodOperatorOidIndexI
 /* allowed values of amoppurpose: */
 #define AMOP_SEARCH		's'		/* operator is for search */
 #define AMOP_ORDER		'o'		/* operator is for ordering */
+#define AMOP_PORTION	'p'		/* operator is for FOR PORTION OF */
 
 #endif							/* EXPOSE_TO_CLIENT_CODE */
 
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 8a5a9fe6422..3d156483325 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -41,6 +41,7 @@ typedef struct TriggerData
 	Tuplestorestate *tg_oldtable;
 	Tuplestorestate *tg_newtable;
 	const Bitmapset *tg_updatedcols;
+	ForPortionOfState *tg_temporal;
 } TriggerData;
 
 /*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 561fdd98f1b..2dcb7379a21 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -41,12 +41,14 @@
 #include "storage/condition_variable.h"
 #include "utils/hsearch.h"
 #include "utils/queryenvironment.h"
+#include "utils/rangetypes.h"
 #include "utils/reltrigger.h"
 #include "utils/sharedtuplestore.h"
 #include "utils/snapshot.h"
 #include "utils/sortsupport.h"
 #include "utils/tuplesort.h"
 #include "utils/tuplestore.h"
+#include "utils/typcache.h"
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -421,6 +423,27 @@ 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/PERIOD named in FOR PORTION OF */
+	Oid		fp_rangeType;		/* the type of the FOR PORTION OF expression */
+	bool	fp_hasPeriod;		/* true iff this is a PERIOD not a range */
+	int		fp_rangeAttno;		/* the attno of the range column (or 0 for a PERIOD) */
+	int		fp_periodStartAttno;	/* the attno of the PERIOD start column (or 0 for a range) */
+	int		fp_periodEndAttno;		/* the attno of the PERIOD end column (or 0 for a range) */
+	Datum	fp_targetRange;		/* the range from FOR PORTION OF */
+	TypeCacheEntry *fp_leftoverstypcache;	/* type cache entry of the range */
+	TupleTableSlot *fp_Existing;		/* slot to store existing target tuple in */
+	TupleTableSlot *fp_Leftover;		/* slot to store leftover */
+} ForPortionOfState;
+
 /*
  * ResultRelInfo
  *
@@ -541,6 +564,9 @@ typedef struct ResultRelInfo
 	List	   *ri_matchedMergeAction;
 	List	   *ri_notMatchedMergeAction;
 
+	/* 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 8f4f8ce6a77..bdada6b4252 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -141,6 +141,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 */
@@ -1530,6 +1533,21 @@ typedef struct RowMarkClause
 	bool		pushedDown;		/* pushed down from higher query level? */
 } RowMarkClause;
 
+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <period-name> FROM <ts> TO <te>
+ *		or FOR PORTION OF <period-name> (<target>)
+ */
+typedef struct ForPortionOfClause
+{
+	NodeTag		type;
+	char	   *range_name;
+	int			location;
+	Node	   *target;
+	Node	   *target_start;
+	Node	   *target_end;
+} ForPortionOfClause;
+
 /*
  * WithClause -
  *	   representation of WITH clause
@@ -1893,12 +1911,13 @@ typedef struct InsertStmt
  */
 typedef struct DeleteStmt
 {
-	NodeTag		type;
-	RangeVar   *relation;		/* relation to delete from */
-	List	   *usingClause;	/* optional using clause for more tables */
-	Node	   *whereClause;	/* qualifications */
-	List	   *returningList;	/* list of expressions to return */
-	WithClause *withClause;		/* WITH clause */
+	NodeTag				type;
+	RangeVar		   *relation;		/* relation to delete from */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
+	List			   *usingClause;	/* optional using clause for more tables */
+	Node			   *whereClause;	/* qualifications */
+	List			   *returningList;	/* list of expressions to return */
+	WithClause		   *withClause;		/* WITH clause */
 } DeleteStmt;
 
 /* ----------------------
@@ -1907,13 +1926,14 @@ typedef struct DeleteStmt
  */
 typedef struct UpdateStmt
 {
-	NodeTag		type;
-	RangeVar   *relation;		/* relation to update */
-	List	   *targetList;		/* the target list (of ResTarget) */
-	Node	   *whereClause;	/* qualifications */
-	List	   *fromClause;		/* optional from clause for more tables */
-	List	   *returningList;	/* list of expressions to return */
-	WithClause *withClause;		/* WITH clause */
+	NodeTag				type;
+	RangeVar		   *relation;		/* relation to update */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
+	List			   *targetList;		/* the target list (of ResTarget) */
+	Node			   *whereClause;	/* qualifications */
+	List			   *fromClause;		/* optional from clause for more tables */
+	List			   *returningList;	/* list of expressions to return */
+	WithClause		   *withClause;		/* WITH clause */
 } UpdateStmt;
 
 /* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b9713ec9aa6..2591f8611d4 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2352,6 +2352,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 b4ef6bc44c9..9cb9b18a4c1 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -243,6 +243,7 @@ typedef struct ModifyTable
 	List	   *rowMarks;		/* PlanRowMarks (non-locking only) */
 	int			epqParam;		/* ID of Param for EvalPlanQual re-eval */
 	OnConflictAction onConflictAction;	/* ON CONFLICT action */
+	Node	   *forPortionOf;	/* FOR PORTION OF clause for UPDATE/DELETE */
 	List	   *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs  */
 	List	   *onConflictSet;	/* INSERT ON CONFLICT DO UPDATE targetlist */
 	List	   *onConflictCols; /* target column numbers for onConflictSet */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4a154606d2b..ea6701d83ca 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2068,4 +2068,30 @@ 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.
+ *
+ * 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	   *targetRange;		/* FOR PORTION OF bounds as a range */
+	Oid			rangeType;			/* type of targetRange */
+	Node	   *overlapsExpr;		/* range && targetRange */
+	List	   *rangeTargetList;	/* List of TargetEntrys to set the time column(s) */
+	Oid			withoutPortionProc;	/* 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 c43d97b48a6..2014cf23234 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -283,7 +283,8 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												List *updateColnosLists,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
-												List *mergeActionLists, int epqParam);
+												List *mergeActionLists,
+												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 5ecdd109466..e512f4b24ea 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 Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
 extern Query *transformStmt(ParseState *pstate, Node *parseTree);
 
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 1d3f7bd37f2..707a517339a 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -330,6 +330,7 @@ PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_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 99d65157365..5e740fef881 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_UPDATE_PORTION,	/* UPDATE 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/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 1339382f91d..9487873ad48 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -143,6 +143,8 @@ extern char get_rel_persistence(Oid relid);
 extern Oid	get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
 extern Oid	get_transform_tosql(Oid typid, Oid langid, List *trftypes);
 extern bool get_typisdefined(Oid typid);
+extern char *get_typname(Oid typid);
+extern bool get_typname_and_namespace(Oid typid, char **typname, char **typnamespace);
 extern int16 get_typlen(Oid typid);
 extern bool get_typbyval(Oid typid);
 extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
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..4376bced4db
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,562 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at tsrange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,1]', '[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 = 'foo';
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO NULL;
+SELECT * FROM for_portion_of_test;
+  id   |                        valid_at                         | name 
+-------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Tue Jan 01 00:00:00 2019") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Tue Jan 01 00:00:00 2019","Tue Jan 15 00:00:00 2019") | one
+(3 rows)
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid1_at tsrange,
+  valid2_at tsrange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,1]', '[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;
+  id   |                        valid1_at                        |                        valid2_at                        | name 
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2025") | one
+(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;
+  id   |                        valid1_at                        |                        valid2_at                        | name 
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Feb 03 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+(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;
+  id   |                        valid1_at                        |                        valid2_at                        | name 
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Wed Jan 01 00:00:00 2025") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+(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;
+  id   |                        valid1_at                        |                        valid2_at                        | name 
+-------+---------------------------------------------------------+---------------------------------------------------------+------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | one
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Thu Jan 01 00:00:00 2015","Mon Jan 15 00:00:00 2018") | foo
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Mon Jan 15 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | bar
+ [1,2) | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | ["Mon Jan 15 00:00:00 2018","Sat Jan 20 00:00:00 2018") | 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 tsrange,
+  name text
+);
+INSERT INTO for_portion_of_test VALUES
+  ('[1,1]', NULL, '1 null'),
+  ('[1,1]', '(,)', '1 unbounded'),
+  ('[1,1]', 'empty', '1 empty'),
+  (NULL, NULL, NULL),
+  (NULL, tsrange('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;
+  id   |                        valid_at                         |     name     
+-------+---------------------------------------------------------+--------------
+ [1,2) |                                                         | 1 null
+ [1,2) | empty                                                   | 1 empty
+       |                                                         | 
+ [1,2) | (,)                                                     | NULL to NULL
+       | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | NULL to NULL
+(5 rows)
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at tsrange NOT NULL,
+  name text NOT NULL,
+	CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-01-05)', 'two'),
+('[3,3]', '[2018-01-01,)', 'three'),
+('[4,4]', '(,2018-04-01)', 'four'),
+('[5,5]', '(,)', 'five')
+;
+--
+-- UPDATE tests
+--
+-- Setting 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,5]';
+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
+                       ^
+-- Setting 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,5]';
+ERROR:  can't directly assign to "valid_at" in a FOR PORTION OF update
+LINE 3: SET valid_at = '[1990-01-01,1999-01-01)'
+            ^
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,3]';
+ERROR:  function pg_catalog.tsrange(integer, integer) does not exist
+LINE 2: FOR PORTION OF valid_at FROM 1 TO 4
+                       ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- Setting 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,3]';
+ERROR:  range lower bound must be less than or equal to range upper bound
+-- Setting 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,3]';
+ERROR:  cannot use subquery in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-...
+                                     ^
+-- Setting 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,3]';
+ERROR:  cannot use column reference in FOR PORTION OF expression
+LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+                                           ^
+-- Setting 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,3]';
+-- 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,3]';
+-- 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,3]';
+-- 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,4]';
+-- 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,4]';
+-- 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,4]';
+-- 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,2]';
+-- 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^2'
+WHERE id = '[5,5]';
+-- 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^3'
+WHERE id = '[5,5]';
+-- 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,1]';
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+  TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,1]';
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+  TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,1]';
+ERROR:  syntax error at or near "'2019-01-01'"
+LINE 4:   TO '2019-01-01'
+             ^
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+  TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,1]';
+-- With a direct target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at (tsrange('2018-03-10', '2018-03-17'))
+SET name = 'one^5'
+WHERE id = '[1,1]';
+-- 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,6]'
+WHERE id = '[1,1]';
+-- 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;
+  id   |                        valid_at                         |   name   
+-------+---------------------------------------------------------+----------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018") | one^2
+ [1,2) | ["Sat Feb 03 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | three
+ [3,4) | ["Fri Jun 01 00:00:00 2018","Tue Jan 01 00:00:00 2030") | three^1
+ [3,4) | ["Tue Jan 01 00:00:00 2030",)                           | three^1*
+ [4,5) | (,"Sun Jan 01 00:00:00 2017")                           | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017")                           | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | five^3
+ [5,6) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | five^3
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | five
+ [5,6) | ["Tue Jan 01 00:00:00 2030",)                           | five*
+ [6,7) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 08:01:00 2018") | one^2
+ [6,7) | ["Thu Mar 01 08:01:00 2018","Thu Mar 01 10:00:00 2018") | one^3
+ [6,7) | ["Thu Mar 01 10:00:00 2018","Sat Mar 03 00:00:00 2018") | one^4
+ [6,7) | ["Sat Mar 03 00:00:00 2018","Sat Mar 10 00:00:00 2018") | one^4
+ [6,7) | ["Sat Mar 10 00:00:00 2018","Sat Mar 17 00:00:00 2018") | one^5
+ [6,7) | ["Sat Mar 17 00:00:00 2018","Wed Apr 04 00:00:00 2018") | one^4
+(22 rows)
+
+--
+-- DELETE tests
+--
+-- 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,5]';
+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
+                       ^
+-- 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,3]';
+ERROR:  range lower bound must be less than or equal to range upper bound
+-- 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,3]';
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,5]';
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,3]';
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,1]';
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,6]';
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |                        valid_at                         |  name   
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017")                           | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017")                           | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") | five
+(11 rows)
+
+-- 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,3]'
+RETURNING *;
+  id   |                        valid_at                         |  name   
+-------+---------------------------------------------------------+---------
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+(1 row)
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+  RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+  IF TG_OP = 'DELETE' THEN
+    RETURN OLD;
+  ELSE
+    RETURN NEW;
+  END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER trg_for_portion_of_before
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,5]';
+NOTICE:  BEFORE UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+NOTICE:  BEFORE INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE:  BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE:  AFTER INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of <NULL>
+NOTICE:  AFTER INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE:  AFTER UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,5]';
+NOTICE:  BEFORE DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+NOTICE:  BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE:  BEFORE INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE:  AFTER INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of <NULL>
+NOTICE:  AFTER INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of <NULL>
+NOTICE:  AFTER DELETE <NULL> of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |                        valid_at                         |  name   
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3
+ [3,4) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017")                           | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017")                           | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | five
+ [5,6) | ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") | five^4
+ [5,6) | ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") | five
+ [5,6) | ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") | five
+(16 rows)
+
+-- 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
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-05-01)', 'two'),
+('[3,3]', '[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,1]';
+DELETE FROM for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+WHERE id = '[2,2]';
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-02-2018,01-10-2018) | one
+ [1,2) | [01-10-2018,02-03-2018) | one^1
+ [1,2) | [02-03-2018,02-10-2018) | one^1
+ [1,2) | [02-10-2018,03-03-2018) | one
+ [1,2) | [03-03-2018,04-04-2018) | one
+ [2,3) | [01-01-2018,01-15-2018) | two
+ [2,3) | [02-15-2018,05-01-2018) | two
+ [3,4) | [01-01-2018,)           | 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,1]', '[2,2]');
+CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,5]', '[6,6]');
+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,3]', '[4,4]');
+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,5]', '[6,6]');
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2010-01-01'), 'one'),
+  ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'),
+  ('[5,5]', daterange('2000-01-01', '2010-01-01'), 'five');
+SELECT * FROM temporal_partitioned;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,01-01-2010) | one
+ [3,4) | [01-01-2000,01-01-2010) | three
+ [5,6) | [01-01-2000,01-01-2010) | 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,1]';
+-- 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,3]';
+-- 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,5]';
+-- 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,4]'
+  WHERE id = '[1,1]';
+-- 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,2]'
+  WHERE id = '[3,3]';
+-- 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,3]'
+  WHERE id = '[5,5]';
+-- Update all partitions at once (each with leftovers)
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [01-01-2000,03-01-2000) | one
+ [1,2) | [03-01-2000,04-01-2000) | one^1
+ [1,2) | [04-01-2000,06-01-2000) | one
+ [1,2) | [07-01-2000,01-01-2010) | one
+ [2,3) | [06-01-2000,07-01-2000) | three^2
+ [3,4) | [01-01-2000,03-01-2000) | three
+ [3,4) | [03-01-2000,04-01-2000) | three^1
+ [3,4) | [04-01-2000,06-01-2000) | three
+ [3,4) | [06-01-2000,07-01-2000) | five^2
+ [3,4) | [07-01-2000,01-01-2010) | three
+ [4,5) | [06-01-2000,07-01-2000) | one^2
+ [5,6) | [01-01-2000,03-01-2000) | five
+ [5,6) | [03-01-2000,04-01-2000) | five^1
+ [5,6) | [04-01-2000,06-01-2000) | five
+ [5,6) | [07-01-2000,01-01-2010) | five
+(15 rows)
+
+SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [01-01-2000,03-01-2000) | one
+ [1,2) | [03-01-2000,04-01-2000) | one^1
+ [1,2) | [04-01-2000,06-01-2000) | one
+ [1,2) | [07-01-2000,01-01-2010) | one
+ [2,3) | [06-01-2000,07-01-2000) | three^2
+(5 rows)
+
+SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at;
+  name   |  id   |        valid_at         
+---------+-------+-------------------------
+ three   | [3,4) | [01-01-2000,03-01-2000)
+ three^1 | [3,4) | [03-01-2000,04-01-2000)
+ three   | [3,4) | [04-01-2000,06-01-2000)
+ five^2  | [3,4) | [06-01-2000,07-01-2000)
+ three   | [3,4) | [07-01-2000,01-01-2010)
+ one^2   | [4,5) | [06-01-2000,07-01-2000)
+(6 rows)
+
+SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
+  name  |        valid_at         |  id   
+--------+-------------------------+-------
+ five   | [01-01-2000,03-01-2000) | [5,6)
+ five^1 | [03-01-2000,04-01-2000) | [5,6)
+ five   | [04-01-2000,06-01-2000) | [5,6)
+ five   | [07-01-2000,01-01-2010) | [5,6)
+(4 rows)
+
+DROP TABLE temporal_partitioned;
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7610b011d68..da079cfc58a 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1942,6 +1942,7 @@ WHERE a1.amopfamily = 0 OR a1.amoplefttype = 0 OR a1.amoprighttype = 0
 SELECT a1.amopfamily, a1.amopstrategy
 FROM pg_amop as a1
 WHERE NOT ((a1.amoppurpose = 's' AND a1.amopsortfamily = 0) OR
+           (a1.amoppurpose = 'p' AND a1.amopsortfamily = 0) OR
            (a1.amoppurpose = 'o' AND a1.amopsortfamily <> 0));
  amopfamily | amopstrategy 
 ------------+--------------
@@ -2008,6 +2009,9 @@ ORDER BY 1, 2, 3;
         783 |           28 | <@
         783 |           29 | <^
         783 |           30 | >^
+        783 |           31 | #
+        783 |           31 | *
+        783 |           32 | @-
         783 |           48 | <@
         783 |           68 | <@
        2742 |            1 | &&
@@ -2088,7 +2092,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(124 rows)
+(127 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index fbb0489a4ff..3395d2150ff 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -985,6 +985,24 @@ 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/PERIOD 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)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) 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;
+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 1950e6f281f..f4b88bcc38d 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3023,6 +3023,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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [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;
+ 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
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [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 c633738c2eb..60c9eedaee8 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -305,6 +305,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
   ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
   ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', '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,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         |  id2   | name  
+-------+-------------------------+--------+-------
+ [1,2) | [01-01-2000,05-01-2000) | [7,8)  | foo
+ [1,2) | [05-01-2000,07-01-2000) | [7,8)  | foo1
+ [1,2) | [07-01-2000,01-01-2010) | [7,8)  | foo
+ [2,3) | [01-01-2000,04-01-2000) | [9,10) | bar
+ [2,3) | [04-01-2000,05-01-2000) | [9,10) | bar2
+ [2,3) | [05-01-2000,06-01-2000) | [9,10) | bar12
+ [2,3) | [06-01-2000,07-01-2000) | [9,10) | bar1
+ [2,3) | [07-01-2000,01-01-2010) | [9,10) | bar
+(8 rows)
+
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+ERROR:  conflicting key value violates exclusion constraint "temporal3_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [01-01-2005,01-01-2006)) conflicts with existing key (id, valid_at)=([1,2), [07-01-2000,01-01-2010)).
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
+ERROR:  conflicting key value violates exclusion constraint "temporal3_uniq"
+DETAIL:  Key (id2, valid_at)=([9,10), [01-01-2005,01-01-2010)) conflicts with existing key (id2, valid_at)=([9,10), [07-01-2000,01-01-2010)).
 DROP TABLE temporal3;
 --
 -- test changing the PK's dependencies
@@ -357,6 +387,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
  [3,4) | [01-01-2000,01-01-2010) | 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,1]';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,4]'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,2]'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,01-15-2000) | one
+ [1,2) | [01-15-2000,02-01-2000) | one2
+ [1,2) | [02-01-2000,02-15-2000) | one2
+ [1,2) | [02-15-2000,02-20-2000) | one
+ [1,2) | [02-25-2000,03-01-2000) | one
+ [2,3) | [01-01-2002,01-01-2003) | three
+ [3,4) | [01-01-2000,01-15-2000) | three
+ [3,4) | [02-15-2000,01-01-2002) | three
+ [3,4) | [01-01-2003,01-01-2010) | three
+ [4,5) | [02-20-2000,02-25-2000) | one
+(10 rows)
+
 DROP TABLE temporal_partitioned;
 -- temporal UNIQUE:
 CREATE TABLE temporal_partitioned (
@@ -392,6 +452,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
  [3,4) | [01-01-2000,01-01-2010) | 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,1]';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,4]'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,2]'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [01-01-2000,01-15-2000) | one
+ [1,2) | [01-15-2000,02-01-2000) | one2
+ [1,2) | [02-01-2000,02-15-2000) | one2
+ [1,2) | [02-15-2000,02-20-2000) | one
+ [1,2) | [02-25-2000,03-01-2000) | one
+ [2,3) | [01-01-2002,01-01-2003) | three
+ [3,4) | [01-01-2000,01-15-2000) | three
+ [3,4) | [02-15-2000,01-01-2002) | three
+ [3,4) | [01-01-2003,01-01-2010) | three
+ [4,5) | [02-20-2000,02-25-2000) | one
+(10 rows)
+
 DROP TABLE temporal_partitioned;
 --
 -- test FK dependencies
@@ -692,13 +782,25 @@ UPDATE temporal_rng SET id = '[7,7]'
 WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+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), ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
 WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- clean up:
 DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
-DELETE FROM temporal_rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]', '[7,7]');
 --
 -- test FK parent updates RESTRICT
 --
@@ -729,13 +831,25 @@ UPDATE temporal_rng SET id = '[7,7]'
 WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+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), ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
 WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- clean up:
 DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
-DELETE FROM temporal_rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]', '[7,7]');
 --
 -- test FK parent deletes NO ACTION
 --
@@ -757,9 +871,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) 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,5]';
+-- 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,5]';
+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), ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 --
 -- test FK parent deletes RESTRICT
 --
@@ -781,9 +908,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('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), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) 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,5]';
+-- 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,5]';
+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), ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "temporal_fk_rng2rng".
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 --
 -- test ON UPDATE/DELETE options
 --
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index b08aaae44b8..cc2f6b46e0c 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..6010c5d94f5
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,423 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+
+-- Works on non-PK columns
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid_at tsrange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,1]', '[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 = 'foo';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO NULL;
+
+SELECT * FROM for_portion_of_test;
+
+-- Works on more than one range
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range,
+  valid1_at tsrange,
+  valid2_at tsrange,
+  name text NOT NULL
+);
+INSERT INTO for_portion_of_test VALUES
+('[1,1]', '[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;
+
+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;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL;
+SELECT * FROM for_portion_of_test;
+
+-- 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 tsrange,
+  name text
+);
+INSERT INTO for_portion_of_test VALUES
+  ('[1,1]', NULL, '1 null'),
+  ('[1,1]', '(,)', '1 unbounded'),
+  ('[1,1]', 'empty', '1 empty'),
+  (NULL, NULL, NULL),
+  (NULL, tsrange('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;
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+  id int4range NOT NULL,
+  valid_at tsrange NOT NULL,
+  name text NOT NULL,
+	CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-01-05)', 'two'),
+('[3,3]', '[2018-01-01,)', 'three'),
+('[4,4]', '(,2018-04-01)', 'four'),
+('[5,5]', '(,)', 'five')
+;
+
+--
+-- UPDATE tests
+--
+
+-- Setting 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,5]';
+
+-- Setting 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,5]';
+
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,3]';
+
+-- Setting 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,3]';
+
+-- Setting 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,3]';
+
+-- Setting 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,3]';
+
+-- Setting 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,3]';
+
+-- 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,3]';
+
+-- 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,3]';
+
+-- 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,4]';
+
+-- 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,4]';
+
+-- 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,4]';
+
+-- 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,2]';
+
+-- 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^2'
+WHERE id = '[5,5]';
+
+-- 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^3'
+WHERE id = '[5,5]';
+
+-- 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,1]';
+
+-- Updating with a shift/reduce conflict
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE
+  TO '2019-01-01'
+SET name = 'one^3'
+WHERE id = '[1,1]';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR
+  TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,1]';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at
+  FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR)
+  TO '2019-01-01'
+SET name = 'one^4'
+WHERE id = '[1,1]';
+
+-- With a direct target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at (tsrange('2018-03-10', '2018-03-17'))
+SET name = 'one^5'
+WHERE id = '[1,1]';
+
+-- 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,6]'
+WHERE id = '[1,1]';
+
+-- 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;
+
+--
+-- DELETE tests
+--
+
+-- 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,5]';
+
+-- 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,3]';
+
+-- 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,3]';
+
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,5]';
+
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,3]';
+
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,1]';
+
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,6]';
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- 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,3]'
+RETURNING *;
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+  RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+  IF TG_OP = 'DELETE' THEN
+    RETURN OLD;
+  ELSE
+    RETURN NEW;
+  END IF;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_for_portion_of_before
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,5]';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,5]';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- 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
+VALUES
+('[1,1]', '[2018-01-02,2018-02-03)', 'one'),
+('[1,1]', '[2018-02-03,2018-03-03)', 'one'),
+('[1,1]', '[2018-03-03,2018-04-04)', 'one'),
+('[2,2]', '[2018-01-01,2018-05-01)', 'two'),
+('[3,3]', '[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,1]';
+
+DELETE FROM for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15'
+WHERE id = '[2,2]';
+
+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,1]', '[2,2]');
+CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]');
+CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,5]', '[6,6]');
+
+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,3]', '[4,4]');
+
+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,5]', '[6,6]');
+
+INSERT INTO temporal_partitioned VALUES
+  ('[1,1]', daterange('2000-01-01', '2010-01-01'), 'one'),
+  ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'),
+  ('[5,5]', 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,1]';
+
+-- 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,3]';
+
+-- 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,5]';
+
+-- 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,4]'
+  WHERE id = '[1,1]';
+
+-- 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,2]'
+  WHERE id = '[3,3]';
+
+-- 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,3]'
+  WHERE id = '[5,5]';
+
+-- 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;
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index 2fe7b6dcc49..2dffe7c5b6a 100644
--- a/src/test/regress/sql/opr_sanity.sql
+++ b/src/test/regress/sql/opr_sanity.sql
@@ -1241,6 +1241,7 @@ WHERE a1.amopfamily = 0 OR a1.amoplefttype = 0 OR a1.amoprighttype = 0
 SELECT a1.amopfamily, a1.amopstrategy
 FROM pg_amop as a1
 WHERE NOT ((a1.amoppurpose = 's' AND a1.amopsortfamily = 0) OR
+           (a1.amoppurpose = 'p' AND a1.amopsortfamily = 0) OR
            (a1.amoppurpose = 'o' AND a1.amopsortfamily <> 0));
 
 -- amopmethod must match owning opfamily's opfmethod
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 3f68cafcd1e..e10added9d9 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -713,6 +713,24 @@ 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/PERIOD 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)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) 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;
+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 eaee0b7e1d7..f814701a43a 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -1541,6 +1541,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;
+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;
+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;
+
 -- 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 641cdd5b19f..95b6da4c076 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -231,6 +231,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
   ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'),
   ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', '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,2]';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+-- conflicting id only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,1]', daterange('2005-01-01', '2006-01-01'), '[8,8]', 'foo3');
+-- conflicting id2 only:
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[3,3]', daterange('2005-01-01', '2010-01-01'), '[9,9]', 'bar3');
 DROP TABLE temporal3;
 
 --
@@ -269,6 +283,22 @@ INSERT INTO temporal_partitioned VALUES
 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;
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,1]';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,4]'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,2]'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
 DROP TABLE temporal_partitioned;
 
 -- temporal UNIQUE:
@@ -287,6 +317,22 @@ INSERT INTO temporal_partitioned VALUES
 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;
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  SET name = 'one2'
+  WHERE id = '[1,1]';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25'
+  SET id = '[4,4]'
+  WHERE name = 'one';
+UPDATE  temporal_partitioned
+  FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01'
+  SET id = '[2,2]'
+  WHERE name = 'three';
+DELETE FROM temporal_partitioned
+  FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15'
+  WHERE id = '[3,3]';
+SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
 DROP TABLE temporal_partitioned;
 
 --
@@ -556,13 +602,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- changing the scalar part fails:
 UPDATE temporal_rng SET id = '[7,7]'
 WHERE id = '[5,5]' AND valid_at = tsrange('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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
 WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- clean up:
 DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
-DELETE FROM temporal_rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]', '[7,7]');
 
 --
 -- test FK parent updates RESTRICT
@@ -591,13 +647,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- changing the scalar part fails:
 UPDATE temporal_rng SET id = '[7,7]'
 WHERE id = '[5,5]' AND valid_at = tsrange('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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
 -- then delete the objecting FK record and the same PK update succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 UPDATE temporal_rng SET valid_at = tsrange('2016-01-01', '2016-02-01')
 WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 -- clean up:
 DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
-DELETE FROM temporal_rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]', '[7,7]');
 --
 -- test FK parent deletes NO ACTION
 --
@@ -617,14 +683,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
 -- a PK delete that fails because both are referenced:
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- 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,5]';
+-- 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,5]';
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 
 --
 -- test FK parent deletes RESTRICT
 --
-
 ALTER TABLE temporal_fk_rng2rng
 	DROP CONSTRAINT temporal_fk_rng2rng_fk;
 ALTER TABLE temporal_fk_rng2rng
@@ -641,9 +717,20 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01');
 -- a PK delete that fails because both are referenced:
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- 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,5]';
+-- 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,5]';
 -- then delete the objecting FK record and the same PK delete succeeds:
 DELETE FROM temporal_fk_rng2rng WHERE id = '[3,3]';
 DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 
 --
 -- test ON UPDATE/DELETE options
-- 
2.42.0

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

---
 src/backend/commands/tablecmds.c              |  42 +-
 src/backend/utils/adt/ri_triggers.c           | 549 +++++++++++-
 src/include/catalog/pg_proc.dat               |  20 +
 .../regress/expected/without_overlaps.out     | 827 +++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 438 +++++++++-
 5 files changed, 1836 insertions(+), 40 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ef7f58f99ac..54b4527aa77 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -516,7 +516,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
 											bool old_check_ok,
 											Oid parentDelTrigger, Oid parentUpdTrigger,
 											bool is_temporal);
-static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, const int16 fkperiodattnum,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
 static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -9903,7 +9903,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
 											  fkdelsetcols, NULL);
-	validateFkOnDeleteSetColumns(numfks, fkattnum,
+	validateFkOnDeleteSetColumns(numfks, fkattnum, fkperiodattnum,
 								 numfkdelsetcols, fkdelsetcols,
 								 fkconstraint->fk_del_set_cols);
 
@@ -10092,12 +10092,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
 	for (int i = 0; i < numfksetcols; i++)
 	{
 		int16		setcol_attnum = fksetcolsattnums[i];
+		/* assume only one PERIOD key column in a foreign key */
+		int16		fkperiod_attnum = fkperiodattnum;
 		bool		seen = false;
 
 		for (int j = 0; j < numfks; j++)
@@ -10107,6 +10110,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 				seen = true;
 				break;
 			}
+			if (fkperiod_attnum == 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 (!seen)
@@ -12744,11 +12754,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 				fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del");
 				break;
 			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_del");
+				break;
 			case FKCONSTR_ACTION_SETNULL:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_del");
+				break;
 			case FKCONSTR_ACTION_SETDEFAULT:
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("action not supported for temporal foreign keys")));
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_del");
 				break;
 			default:
 				elog(ERROR, "unrecognized FK action type: %d",
@@ -12835,11 +12853,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 				fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd");
 				break;
 			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_upd");
+				break;
 			case FKCONSTR_ACTION_SETNULL:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_upd");
+				break;
 			case FKCONSTR_ACTION_SETDEFAULT:
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("action not supported for temporal foreign keys")));
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("TRI_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 4d17b3877bd..001f6ed4d7e 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -84,6 +84,12 @@
 #define RI_PLAN_SETNULL_ONUPDATE		7
 #define RI_PLAN_SETDEFAULT_ONDELETE		8
 #define RI_PLAN_SETDEFAULT_ONUPDATE		9
+#define TRI_PLAN_CASCADE_ONDELETE		10
+#define TRI_PLAN_CASCADE_ONUPDATE		11
+#define TRI_PLAN_SETNULL_ONUPDATE		12
+#define TRI_PLAN_SETNULL_ONDELETE		13
+#define TRI_PLAN_SETDEFAULT_ONUPDATE	14
+#define TRI_PLAN_SETDEFAULT_ONDELETE	15
 
 #define MAX_QUOTED_NAME_LEN  (NAMEDATALEN*2+3)
 #define MAX_QUOTED_REL_NAME_LEN  (MAX_QUOTED_NAME_LEN*2)
@@ -193,6 +199,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,
@@ -1410,6 +1417,127 @@ TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
 	return ri_restrict((TriggerData *) fcinfo->context, false);
 }
 
+/*
+ * TRI_FKey_cascade_del -
+ *
+ * Cascaded delete foreign key references at delete event on temporal PK table.
+ */
+Datum
+TRI_FKey_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, "TRI_FKey_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_cascading_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, TRI_PLAN_CASCADE_ONDELETE);
+
+	if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+	{
+		StringInfoData querybuf;
+		char		fkrelname[MAX_QUOTED_REL_NAME_LEN];
+		char		attname[MAX_QUOTED_NAME_LEN];
+		char		paramname[16];
+		const char *querysep;
+		Oid			queryoids[RI_MAX_NUMKEYS + 1];
+		const char *fk_only;
+
+		/* ----------
+		 * The query string built is
+		 *  DELETE FROM [ONLY] <fktable>
+		 *  FOR PORTION OF $fkatt FROM lower(${n+1}) TO upper(${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 FROM lower($%d) TO upper($%d)",
+						 fk_only, fkrelname, attname, riinfo->nkeys + 1, 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, targetRange,
+					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);
+}
+
+
 /*
  * TRI_FKey_noaction_upd -
  *
@@ -1447,6 +1575,415 @@ TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
 	return ri_restrict((TriggerData *) fcinfo->context, false);
 }
 
+/*
+ * TRI_FKey_cascade_upd -
+ *
+ * Cascaded update foreign key references at update event on temporal PK table.
+ */
+Datum
+TRI_FKey_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, "TRI_FKey_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_cascading_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, TRI_PLAN_CASCADE_ONUPDATE);
+
+	if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
+	{
+		StringInfoData querybuf;
+		StringInfoData qualbuf;
+		char		fkrelname[MAX_QUOTED_REL_NAME_LEN];
+		char		attname[MAX_QUOTED_NAME_LEN];
+		char		paramname[16];
+		const char *querysep;
+		const char *qualsep;
+		Oid		 queryoids[2 * RI_MAX_NUMKEYS + 1];
+		const char *fk_only;
+
+		/* ----------
+		 * The query string built is
+		 *  UPDATE [ONLY] <fktable>
+		 *		  FOR PORTION OF $fkatt FROM lower(${2n+1}) TO upper(${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 FROM lower($%d) TO upper($%d) SET",
+						 fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1, 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, targetRange,
+					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);
+}
+
+/*
+ * TRI_FKey_setnull_del -
+ *
+ * Set foreign key references to NULL values at delete event on PK table.
+ */
+Datum
+TRI_FKey_setnull_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_del", RI_TRIGTYPE_DELETE);
+
+	/* Share code with UPDATE case */
+	return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setnull_upd -
+ *
+ * Set foreign key references to NULL at update event on PK table.
+ */
+Datum
+TRI_FKey_setnull_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with DELETE case */
+	return tri_set((TriggerData *) fcinfo->context, true, RI_TRIGTYPE_UPDATE);
+}
+
+/*
+ * TRI_FKey_setdefault_del -
+ *
+ * Set foreign key references to defaults at delete event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_del", RI_TRIGTYPE_DELETE);
+
+	/* Share code with UPDATE case */
+	return tri_set((TriggerData *) fcinfo->context, false, RI_TRIGTYPE_DELETE);
+}
+
+/*
+ * TRI_FKey_setdefault_upd -
+ *
+ * Set foreign key references to defaults at update event on PK table.
+ */
+Datum
+TRI_FKey_setdefault_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "TRI_FKey_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 than more than the PK's duration,
+	 * trimmed by an original FOR PORTION OF if necessary.
+	 */
+	targetRange = restrict_cascading_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
+				? TRI_PLAN_SETNULL_ONUPDATE
+				: TRI_PLAN_SETDEFAULT_ONUPDATE;
+			break;
+		case RI_TRIGTYPE_DELETE:
+			queryno = is_set_null
+				? TRI_PLAN_SETNULL_ONDELETE
+				: TRI_PLAN_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 FROM lower(${n+1}) TO upper(${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 FROM lower($%d) TO upper($%d) SET",
+						 fk_only, fkrelname, attname, riinfo->nkeys + 1, 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, targetRange,
+					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 -
  *
@@ -2583,8 +3120,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
@@ -2619,8 +3156,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 		ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk,
 						 vals, nulls);
 		if (oldslot)
+		{
 			ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
 							 vals + riinfo->nkeys, nulls + riinfo->nkeys);
+		}
 	}
 	else
 	{
@@ -3331,8 +3870,14 @@ 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_TRI_FKEY_CASCADE_DEL:
+		case F_TRI_FKEY_CASCADE_UPD:
 		case F_TRI_FKEY_RESTRICT_DEL:
 		case F_TRI_FKEY_RESTRICT_UPD:
+		case F_TRI_FKEY_SETNULL_DEL:
+		case F_TRI_FKEY_SETNULL_UPD:
+		case F_TRI_FKEY_SETDEFAULT_DEL:
+		case F_TRI_FKEY_SETDEFAULT_UPD:
 		case F_TRI_FKEY_NOACTION_DEL:
 		case F_TRI_FKEY_NOACTION_UPD:
 			return RI_TRIGGER_PK;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8f95e082170..d233b9861cd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3952,6 +3952,12 @@
 { oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
   proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger',
   proargtypes => '', prosrc => 'TRI_FKey_check_upd' },
+{ oid => '6124', descr => 'temporal referential integrity ON DELETE CASCADE',
+  proname => 'TRI_FKey_cascade_del', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_cascade_del' },
+{ oid => '6125', descr => 'temporal referential integrity ON UPDATE CASCADE',
+  proname => 'TRI_FKey_cascade_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_cascade_upd' },
 { oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
   proname => 'TRI_FKey_restrict_del', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
@@ -3960,6 +3966,20 @@
   proname => 'TRI_FKey_restrict_upd', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
   prosrc => 'TRI_FKey_restrict_upd' },
+{ oid => '6128', descr => 'temporal referential integrity ON DELETE SET NULL',
+  proname => 'TRI_FKey_setnull_del', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_setnull_del' },
+{ oid => '6129', descr => 'temporal referential integrity ON UPDATE SET NULL',
+  proname => 'TRI_FKey_setnull_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'TRI_FKey_setnull_upd' },
+{ oid => '6130', descr => 'temporal referential integrity ON DELETE SET DEFAULT',
+  proname => 'TRI_FKey_setdefault_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_setdefault_del' },
+{ oid => '6131', descr => 'temporal referential integrity ON UPDATE SET DEFAULT',
+  proname => 'TRI_FKey_setdefault_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'TRI_FKey_setdefault_upd' },
 { oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
   proname => 'TRI_FKey_noaction_del', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 60c9eedaee8..4f8b08c4ce3 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -927,30 +927,184 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 --
 -- test ON UPDATE/DELETE options
 --
+-- TOC:
+-- parent updates CASCADE
+-- parent deletes CASCADE
+-- parent updates SET NULL
+-- parent deletes SET NULL
+-- parent updates SET DEFAULT
+-- parent deletes SET DEFAULT
+-- parent updates CASCADE (two scalar cols)
+-- parent deletes CASCADE (two scalar cols)
+-- parent updates SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols, SET NULL subset)
+-- parent updates SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
 -- test FK parent updates CASCADE
 INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 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 CASCADE ON UPDATE CASCADE;
-ERROR:  action not supported for temporal foreign keys
+-- leftovers on both sides:
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [9,10)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(2 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(1 row)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
 -- test FK parent updates SET NULL
-INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 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:  action not supported for temporal foreign keys
+-- leftovers on both sides:
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | 
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | 
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | 
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | 
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | 
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | 
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
 -- test FK parent updates SET DEFAULT
 INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
-INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 ALTER TABLE temporal_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
@@ -958,7 +1112,461 @@ ALTER TABLE temporal_fk_rng2rng
 		FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng
 		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
-ERROR:  action not supported for temporal foreign keys
+-- leftovers on both sides:
+UPDATE temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+-- non-FPO update:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id 
+-----------+---------------------------------------------------------+-----------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent updates CASCADE (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)      | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)      | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8)      | [6,7)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [9,10)     | [8,9)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes CASCADE (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(2 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+ id | valid_at | parent_id1 | parent_id2 
+----+----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(1 row)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent updates SET NULL (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |            | 
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |            | 
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |            | 
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET NULL (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | 
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |            | 
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |            | 
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |            | 
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET NULL (two scalar cols, SET NULL subset)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- 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,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") |            | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") |            | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") |            | [6,7)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") |            | [8,9)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent updates SET DEFAULT (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)     | [-1,0)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)     | [-1,0)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET DEFAULT (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)     | [-1,0)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)     | [-1,0)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)     | [-1,0)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- test FK parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[6,6]', tsrange(null, null));
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- 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,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)      | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)      | [6,7)
+(3 rows)
+
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [100,101) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)     | [6,7)
+ [100,101) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)     | [6,7)
+ [100,101) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)     | [6,7)
+(3 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[8,8]', tsrange(null, null));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+    id     |                        valid_at                         | parent_id1 | parent_id2 
+-----------+---------------------------------------------------------+------------+------------
+ [200,201) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)     | [8,9)
+ [200,201) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)      | [8,9)
+(2 rows)
+
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+-- 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 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk3_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng3 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk3_rng2rng WHERE id = '[5,5]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [5,6) | [01-01-2018,01-01-2019) | [8,9)
+ [5,6) | [01-01-2020,01-01-2021) | [8,9)
+(2 rows)
+
+DROP TABLE temporal_fk3_rng2rng;
+DROP TABLE temporal_rng3;
+DROP TYPE mydaterange;
 -- FK between partitioned tables
 CREATE TABLE temporal_partitioned_rng (
 	id int4range,
@@ -966,8 +1574,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
 	CONSTRAINT temporal_paritioned_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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
 INSERT INTO temporal_partitioned_rng VALUES
   ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
   ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -980,8 +1588,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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tfkp2 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
 -- partitioned FK child inserts
 INSERT INTO temporal_partitioned_fk_rng2rng VALUES
   ('[1,1]', daterange('2000-01-01', '2000-02-15'), '[1,1]'),
@@ -1016,7 +1624,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,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 -- clean up:
 DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -1028,7 +1636,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- should fail:
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 -- clean up:
 DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -1050,7 +1658,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,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 -- clean up:
 DELETE FROM temporal_partitioned_fk_rng2rng WHERE parent_id = '[5,5]';
@@ -1062,35 +1670,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- should fail:
 DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' 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_parent_id_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_fkey1" on table "temporal_partitioned_fk_rng2rng"
 DETAIL:  Key (id, valid_at)=([5,6), [01-01-2018,02-01-2018)) is still referenced from table "temporal_partitioned_fk_rng2rng".
 -- partitioned FK parent updates CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[4,4]', daterange('2018-01-01', '2021-01-01'), '[6,6]');
 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:  action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [4,5) | [01-01-2019,01-01-2020) | [7,8)
+ [4,5) | [01-01-2018,01-01-2019) | [6,7)
+ [4,5) | [01-01-2020,01-01-2021) | [6,7)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [4,5) | [01-01-2019,01-01-2020) | [7,8)
+ [4,5) | [01-01-2018,01-01-2019) | [7,8)
+ [4,5) | [01-01-2020,01-01-2021) | [7,8)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[10,10]', daterange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_partitioned_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,10]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [10,11) | [01-01-2018,01-01-2020) | [16,17)
+ [10,11) | [01-01-2020,01-01-2021) | [15,16)
+(2 rows)
+
 -- partitioned FK parent deletes CASCADE
+INSERT INTO temporal_partitioned_rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[5,5]', daterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [5,6) | [01-01-2018,01-01-2019) | [8,9)
+ [5,6) | [01-01-2020,01-01-2021) | [8,9)
+(2 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,11]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [11,12) | [01-01-2020,01-01-2021) | [17,18)
+(1 row)
+
 -- partitioned FK parent updates SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'), '[9,9]');
 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:  action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [6,7) | [01-01-2019,01-01-2020) | 
+ [6,7) | [01-01-2018,01-01-2019) | [9,10)
+ [6,7) | [01-01-2020,01-01-2021) | [9,10)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [6,7) | [01-01-2019,01-01-2020) | 
+ [6,7) | [01-01-2018,01-01-2019) | 
+ [6,7) | [01-01-2020,01-01-2021) | 
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_partitioned_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,12]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [12,13) | [01-01-2018,01-01-2020) | 
+ [12,13) | [01-01-2020,01-01-2021) | [18,19)
+(2 rows)
+
 -- partitioned FK parent deletes SET NULL
+INSERT INTO temporal_partitioned_rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[7,7]', daterange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [7,8) | [01-01-2019,01-01-2020) | 
+ [7,8) | [01-01-2018,01-01-2019) | [11,12)
+ [7,8) | [01-01-2020,01-01-2021) | [11,12)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [7,8) | [01-01-2019,01-01-2020) | 
+ [7,8) | [01-01-2018,01-01-2019) | 
+ [7,8) | [01-01-2020,01-01-2021) | 
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[13,13]', daterange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,13]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [13,14) | [01-01-2018,01-01-2020) | 
+ [13,14) | [01-01-2020,01-01-2021) | [20,21)
+(2 rows)
+
 -- partitioned FK parent updates SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[0,0]', daterange(null, null));
+INSERT INTO temporal_partitioned_rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'), '[12,12]');
 ALTER TABLE temporal_partitioned_fk_rng2rng
-  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id SET DEFAULT '[0,0]',
 	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 DEFAULT ON UPDATE SET DEFAULT;
-ERROR:  action not supported for temporal foreign keys
+UPDATE temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [8,9) | [01-01-2019,01-01-2020) | [0,1)
+ [8,9) | [01-01-2018,01-01-2019) | [12,13)
+ [8,9) | [01-01-2020,01-01-2021) | [12,13)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+  id   |        valid_at         | parent_id 
+-------+-------------------------+-----------
+ [8,9) | [01-01-2019,01-01-2020) | [0,1)
+ [8,9) | [01-01-2018,01-01-2019) | [0,1)
+ [8,9) | [01-01-2020,01-01-2021) | [0,1)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_partitioned_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,14]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [14,15) | [01-01-2018,01-01-2020) | [0,1)
+ [14,15) | [01-01-2020,01-01-2021) | [22,23)
+(2 rows)
+
 -- partitioned FK parent deletes SET DEFAULT
+INSERT INTO temporal_partitioned_rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+   id   |        valid_at         | parent_id 
+--------+-------------------------+-----------
+ [9,10) | [01-01-2019,01-01-2020) | [0,1)
+ [9,10) | [01-01-2018,01-01-2019) | [14,15)
+ [9,10) | [01-01-2020,01-01-2021) | [14,15)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+   id   |        valid_at         | parent_id 
+--------+-------------------------+-----------
+ [9,10) | [01-01-2019,01-01-2020) | [0,1)
+ [9,10) | [01-01-2018,01-01-2019) | [0,1)
+ [9,10) | [01-01-2020,01-01-2021) | [0,1)
+(3 rows)
+
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[15,15]', daterange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,15]';
+   id    |        valid_at         | parent_id 
+---------+-------------------------+-----------
+ [15,16) | [01-01-2018,01-01-2020) | [0,1)
+ [15,16) | [01-01-2020,01-01-2021) | [24,25)
+(2 rows)
+
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 95b6da4c076..8aeb245cbe9 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -735,31 +735,114 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 --
 -- test ON UPDATE/DELETE options
 --
+-- TOC:
+-- parent updates CASCADE
+-- parent deletes CASCADE
+-- parent updates SET NULL
+-- parent deletes SET NULL
+-- parent updates SET DEFAULT
+-- parent deletes SET DEFAULT
+-- parent updates CASCADE (two scalar cols)
+-- parent deletes CASCADE (two scalar cols)
+-- parent updates SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols, SET NULL subset)
+-- parent updates SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
 
 -- test FK parent updates CASCADE
 INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 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 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
 
 -- test FK parent updates SET NULL
-INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]');
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
 
 -- test FK parent updates SET DEFAULT
 INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
-INSERT INTO temporal_rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'));
-INSERT INTO temporal_fk_rng2rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]');
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
 ALTER TABLE temporal_fk_rng2rng
   ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
@@ -767,6 +850,271 @@ ALTER TABLE temporal_fk_rng2rng
 		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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng WHERE id IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent updates CASCADE (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes CASCADE (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent updates SET NULL (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET NULL (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET NULL (two scalar cols, SET NULL subset)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- 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,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent updates SET DEFAULT (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[-1,-1]', tsrange(null, null));
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+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,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET DEFAULT (two scalar cols)
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- test FK parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[6,6]', tsrange(null, null));
+INSERT INTO temporal_rng2 VALUES ('[6,6]', '[6,6]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[100,100]', tsrange('2018-01-01', '2021-01-01'), '[6,6]', '[6,6]');
+-- 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,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[100,100]';
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[8,8]', '[8,8]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_rng2 VALUES ('[-1,-1]', '[8,8]', tsrange(null, null));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[200,200]', tsrange('2018-01-01', '2021-01-01'), '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[200,200]';
+-- clean up
+DELETE FROM temporal_fk2_rng2rng WHERE id IN ('[100,100]', '[200,200]');
+DELETE FROM temporal_rng2 WHERE id1 IN ('[6,6]', '[7,7]', '[8,8]', '[9,9]');
+
+-- 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 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk3_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng3 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk3_rng2rng WHERE id = '[5,5]';
+
+DROP TABLE temporal_fk3_rng2rng;
+DROP TABLE temporal_rng3;
+DROP TYPE mydaterange;
 
 -- FK between partitioned tables
 
@@ -776,8 +1124,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
 	CONSTRAINT temporal_paritioned_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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned_rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
 INSERT INTO temporal_partitioned_rng VALUES
   ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'),
   ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'),
@@ -791,8 +1139,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,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]');
-CREATE TABLE tfkp2 partition OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]');
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[1,1]', '[3,3]', '[5,5]', '[7,7]', '[9,9]', '[11,11]', '[13,13]', '[15,15]', '[17,17]', '[19,19]', '[21,21]', '[23,23]');
+CREATE TABLE tfkp2 PARTITION OF temporal_partitioned_fk_rng2rng FOR VALUES IN ('[0,0]', '[2,2]', '[4,4]', '[6,6]', '[8,8]', '[10,10]', '[12,12]', '[14,14]', '[16,16]', '[18,18]', '[20,20]', '[22,22]', '[24,24]');
 
 -- partitioned FK child inserts
 
@@ -876,37 +1224,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange
 
 -- partitioned FK parent updates CASCADE
 
+INSERT INTO temporal_partitioned_rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[4,4]', daterange('2018-01-01', '2021-01-01'), '[6,6]');
 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+UPDATE temporal_partitioned_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[4,4]';
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[15,15]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[10,10]', daterange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_partitioned_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[10,10]';
 
 -- partitioned FK parent deletes CASCADE
 
+INSERT INTO temporal_partitioned_rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[5,5]', daterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[5,5]';
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[17,17]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[11,11]';
+
 -- partitioned FK parent updates SET NULL
 
+INSERT INTO temporal_partitioned_rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[6,6]', daterange('2018-01-01', '2021-01-01'), '[9,9]');
 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,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+UPDATE temporal_partitioned_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[6,6]';
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[18,18]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_partitioned_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[12,12]';
 
 -- partitioned FK parent deletes SET NULL
 
+INSERT INTO temporal_partitioned_rng VALUES ('[11,11]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[7,7]', daterange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[7,7]';
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[20,20]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[13,13]', daterange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[13,13]';
+
 -- partitioned FK parent updates SET DEFAULT
 
+INSERT INTO temporal_partitioned_rng VALUES ('[0,0]', daterange(null, null));
+INSERT INTO temporal_partitioned_rng VALUES ('[12,12]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[8,8]', daterange('2018-01-01', '2021-01-01'), '[12,12]');
 ALTER TABLE temporal_partitioned_fk_rng2rng
-  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id SET DEFAULT '[0,0]',
 	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 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,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+UPDATE temporal_partitioned_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[8,8]';
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[22,22]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_partitioned_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[14,14]';
 
 -- partitioned FK parent deletes SET DEFAULT
 
+INSERT INTO temporal_partitioned_rng VALUES ('[14,14]', daterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[9,9]', daterange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_partitioned_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[9,9]';
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_partitioned_rng VALUES ('[24,24]', daterange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[15,15]', daterange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_partitioned_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_partitioned_fk_rng2rng WHERE id = '[15,15]';
+
 DROP TABLE temporal_partitioned_fk_rng2rng;
 DROP TABLE temporal_partitioned_rng;
-- 
2.42.0

From 4d3f04569efac5e2598dc75859577ef3a3f82035 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v23 8/8] Add PERIODs

- Added parsing for SQL:2011 syntax to define an application-time PERIOD on a
  table (in both CREATE TABLE and ALTER TABLE). Make sure we create the PERIOD
  after columns are known (since PERIODs can refer to them) but before
  constraints are handled (since PERIODs can appear in them).
- Added ALTER TABLE DROP support for PERIODs.
- Created postgres.pg_period table.
- Created information_schema.periods view.
- Added pg_dump support.
- Added tests and documentation.
- Automatically define a constraint for each PERIOD requiring the start column
  to be less than the end column.
- When creating a PERIOD, choose an appropriate range type we can use to
  implement PERIOD-related operations. You can choose one explicitly if there
  is ambiguity (due to multiple range types created over the same base type).
---
 doc/src/sgml/catalogs.sgml                    | 112 +++
 doc/src/sgml/ddl.sgml                         |  58 ++
 doc/src/sgml/information_schema.sgml          |  63 ++
 doc/src/sgml/ref/alter_table.sgml             |  25 +
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/create_table.sgml            |  40 +
 doc/src/sgml/ref/delete.sgml                  |   3 +-
 doc/src/sgml/ref/update.sgml                  |   3 +-
 src/backend/catalog/Makefile                  |   2 +
 src/backend/catalog/aclchk.c                  |   2 +
 src/backend/catalog/dependency.c              |   9 +
 src/backend/catalog/heap.c                    |  76 ++
 src/backend/catalog/information_schema.sql    |  23 +-
 src/backend/catalog/meson.build               |   1 +
 src/backend/catalog/objectaddress.c           |  73 ++
 src/backend/catalog/pg_period.c               | 132 +++
 src/backend/catalog/sql_features.txt          |   2 +-
 src/backend/commands/alter.c                  |   1 +
 src/backend/commands/comment.c                |  10 +
 src/backend/commands/dropcmds.c               |   1 +
 src/backend/commands/event_trigger.c          |   4 +
 src/backend/commands/seclabel.c               |   1 +
 src/backend/commands/tablecmds.c              | 749 +++++++++++++++++-
 src/backend/commands/view.c                   |   4 +-
 src/backend/nodes/nodeFuncs.c                 |   3 +
 src/backend/parser/gram.y                     |  43 +-
 src/backend/parser/parse_relation.c           |  10 +
 src/backend/parser/parse_utilcmd.c            | 145 +++-
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/period.c                |  56 ++
 src/backend/utils/cache/lsyscache.c           |  87 ++
 src/backend/utils/cache/syscache.c            |  21 +
 src/bin/pg_dump/pg_backup_archiver.c          |   1 +
 src/bin/pg_dump/pg_dump.c                     | 173 +++-
 src/bin/pg_dump/pg_dump.h                     |  14 +
 src/bin/pg_dump/pg_dump_sort.c                |   7 +
 src/bin/psql/describe.c                       |  36 +
 src/include/catalog/dependency.h              |   1 +
 src/include/catalog/heap.h                    |   4 +
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/pg_index.h                |   1 -
 src/include/catalog/pg_period.h               |  54 ++
 src/include/catalog/pg_range.h                |   1 +
 src/include/commands/tablecmds.h              |   4 +-
 src/include/nodes/parsenodes.h                |  39 +-
 src/include/parser/parse_utilcmd.h            |   1 +
 src/include/utils/lsyscache.h                 |   3 +
 src/include/utils/period.h                    |  19 +
 src/include/utils/syscache.h                  |   3 +
 .../test_ddl_deparse/test_ddl_deparse.c       |   6 +
 src/test/regress/expected/periods.out         | 177 +++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/periods.sql              | 119 +++
 53 files changed, 2389 insertions(+), 39 deletions(-)
 create mode 100644 src/backend/catalog/pg_period.c
 create mode 100644 src/backend/utils/adt/period.c
 create mode 100644 src/include/catalog/pg_period.h
 create mode 100644 src/include/utils/period.h
 create mode 100644 src/test/regress/expected/periods.out
 create mode 100644 src/test/regress/sql/periods.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 1ac8b74fe3d..5c045bce186 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -230,6 +230,11 @@
       <entry>information about partition key of tables</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-period"><structname>pg_period</structname></link></entry>
+      <entry>periods</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-policy"><structname>pg_policy</structname></link></entry>
       <entry>row-security policies</entry>
@@ -5726,6 +5731,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        are simple references.
       </para></entry>
      </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+
+ <sect1 id="catalog-pg-period">
+  <title><structname>pg_period</structname></title>
+
+  <indexterm zone="catalog-pg-period">
+   <primary>pg_period</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_period</structname> stores
+   information about system and application time periods.
+  </para>
+
+  <para>
+   Periods are described in <xref linkend="ddl-periods"/>.
+  </para>
+
+  <table>
+   <title><structname>pg_period</structname> Columns</title>
+
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pername</structfield> <type>text</type>
+      </para>
+      <para>
+       Period name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>perrelid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-class"><structname>pg_class</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The table this period belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>perstart</structfield> <type>int2</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       The number of the start column
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>perend</structfield> <type>int2</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       The number of the end column
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>perrange</structfield> <type>int2</type>
+       (references <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>.<structfield>attnum</structfield>)
+      </para>
+      <para>
+       The number of the <literal>GENERATED</literal> column that implements the <literal>PERIOD</literal>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>perconstraint</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the period's <literal>CHECK</literal> constraint
+      </para></entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index fc03a349f0f..7e5539599e0 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1375,6 +1375,64 @@ CREATE TABLE circles (
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-periods">
+  <title>Periods</title>
+
+  <para>
+   Periods are definitions on a table that associate a period name with a start
+   column and an end column.  Both columns must be of exactly the same type
+   (including collation), have a btree operator class, and the start column
+   value must be strictly less than the end column value.
+  </para>
+
+  <para>
+   There are two types of periods: application and system.  System periods are
+   distinguished by their name which must be <literal>SYSTEM_TIME</literal>.  Any
+   other name is an application period.
+  </para>
+
+  <sect2 id="ddl-periods-application-periods">
+  <title>Application Periods</title>
+
+   <indexterm>
+    <primary>period</primary>
+    <secondary>application</secondary>
+   </indexterm>
+
+   <para>
+    Application periods are defined on a table using the following syntax:
+   </para>
+
+<programlisting>
+CREATE TABLE billing_addresses (
+  customer_id integer,
+  address_id integer,
+  valid_from date,
+  valid_to date,
+  <emphasis>PERIOD FOR validity (valid_from, valid_to)</emphasis>
+);
+</programlisting>
+
+   <para>
+    Application periods can be used to define temporal primary and foreign keys.
+    Any table with a temporal primary key is a temporal table and supports temporal update and delete commands.
+   </para>
+  </sect2>
+
+  <sect2 id="ddl-periods-system-periods">
+   <title>System Periods</title>
+
+   <indexterm>
+    <primary>period</primary>
+    <secondary>system</secondary>
+   </indexterm>
+
+   <para>
+    Periods for <literal>SYSTEM_TIME</literal> are currently not implemented.
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9e66be4e83d..558cc7b3f15 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -4171,6 +4171,69 @@ ORDER BY c.ordinal_position;
   </table>
  </sect1>
 
+ <sect1 id="infoschema-periods">
+  <title><literal>periods</literal></title>
+
+  <para>
+   The view <literal>parameters</literal> contains information about the
+   periods of all tables in the current database.  The start and end column
+   names are only shown if the current user has access to them (by way of being
+   the owner or having some privilege).
+  </para>
+
+  <table>
+   <title><literal>periods</literal> Columns</title>
+
+   <tgroup cols="3">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Data Type</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry><literal>table_catalog</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the database containing the period (always the current database)</entry>
+     </row>
+
+     <row>
+      <entry><literal>table_schema</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the schema containing the period</entry>
+     </row>
+
+     <row>
+      <entry><literal>table_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the table containing the period</entry>
+     </row>
+
+     <row>
+      <entry><literal>period_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the period</entry>
+     </row>
+
+     <row>
+      <entry><literal>start_column_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the start column for the period</entry>
+     </row>
+
+     <row>
+      <entry><literal>end_column_name</literal></entry>
+      <entry><type>sql_identifier</type></entry>
+      <entry>Name of the end column for the period</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="infoschema-referential-constraints">
   <title><literal>referential_constraints</literal></title>
 
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 9670671107e..eca9c3ba545 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -61,6 +61,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
     VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
     DROP CONSTRAINT [ IF EXISTS ]  <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+    ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+    DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
     DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -585,6 +587,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-altertable-desc-add-period">
+    <term><literal>ADD PERIOD FOR</literal></term>
+    <listitem>
+     <para>
+      This form adds a new period to a table using the same syntax as
+      <xref linkend="sql-createtable"/>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="sql-altertable-desc-drop-period">
+    <term><literal>DROP PERIOD FOR</literal></term>
+    <listitem>
+     <para>
+      This form drops the specified period on a table.  The start and end
+      columns will not be dropped by this command but the
+      <literal>CHECK</literal> constraint will be.  You will need to say
+      <literal>CASCADE</literal> if anything outside the table depends on the
+      column.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-altertable-desc-disable-enable-trigger">
     <term><literal>DISABLE</literal>/<literal>ENABLE [ REPLICA | ALWAYS ] TRIGGER</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b133..49c2df99449 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -44,6 +44,7 @@ COMMENT ON
   OPERATOR <replaceable class="parameter">operator_name</replaceable> (<replaceable class="parameter">left_type</replaceable>, <replaceable class="parameter">right_type</replaceable>) |
   OPERATOR CLASS <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
+  PERIOD <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">period_name</replaceable> |
   POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
   PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
@@ -341,6 +342,7 @@ COMMENT ON OPERATOR ^ (text, text) IS 'Performs intersection of two texts';
 COMMENT ON OPERATOR - (NONE, integer) IS 'Unary minus';
 COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees';
 COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees';
+COMMENT ON PERIOD my_table.my_column IS 'Sales promotion validity';
 COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users';
 COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report';
 COMMENT ON PUBLICATION alltables IS 'Publishes all operations on all tables';
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 69081759d43..79bbd2402a0 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
   { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+    | <replaceable>period_definition</replaceable>
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -37,6 +38,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
     OF <replaceable class="parameter">type_name</replaceable> [ (
   { <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+    | <replaceable>period_definition</replaceable>
     | <replaceable>table_constraint</replaceable> }
     [, ... ]
 ) ]
@@ -49,6 +51,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable>
     PARTITION OF <replaceable class="parameter">parent_table</replaceable> [ (
   { <replaceable class="parameter">column_name</replaceable> [ WITH OPTIONS ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+    | <replaceable>period_definition</replaceable>
     | <replaceable>table_constraint</replaceable> }
     [, ... ]
 ) ] { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }
@@ -73,6 +76,11 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
     [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
 
+<phrase>and <replaceable class="parameter">period_definition</replaceable> is:</phrase>
+
+PERIOD FOR { <replaceable class="parameter">period_name</replaceable> | SYSTEM_TIME } ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> )
+[ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+
 <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
@@ -140,6 +148,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    name as any existing data type in the same schema.
   </para>
 
+  <para>
+   Periods my be defined on tables, specifying that two existing columns
+   represent start and end values for the period.  Periods may have any name
+   that doesn't conflict with a column name, but the name
+   <literal>SYSTEM_TIME</literal> is special, used for versioning tables.
+   System periods are not yet implemented.  See <xref linkend="ddl-periods"/> for more details.
+  </para>
+
   <para>
    The optional constraint clauses specify constraints (tests) that
    new or updated rows must satisfy for an insert or update operation
@@ -800,6 +816,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-period">
+    <term><literal>PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]</literal></term>
+    <listitem>
+     <para>
+      A period definition gives semantic meaning to two existing columns of
+      the table.  It defines a "start column" and an "end column" where the
+      start value is strictly less than the end value.  A
+      <literal>CHECK</literal> constraint is automatically created to enforce
+      this. You can specify the name of that constraint with the
+      <literal>check_constraint_name</literal> <replaceable class="parameter">period_option</replaceable>.
+     </para>
+
+     <para>
+      Both columns must have exactly the same type and must have a range type
+      defined from their base type.  If there are several range types for that
+      base type, you must specify which one you want by using the
+      <literal>rangetype</literal> <replaceable class="parameter">period_option</replaceable>.
+      Any base type is allowed, as long as it has a range type, although it is
+      expected that most periods will use temporal types like <literal>timestamptz</literal>
+      or <literal>date</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-constraint">
     <term><literal>CONSTRAINT <replaceable class="parameter">constraint_name</replaceable></literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 1c7275341ea..b388be7508c 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -56,7 +56,8 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
   </para>
 
   <para>
-   If the table has a range or multirange column,
+   If the table has a range or multirange column, or a
+   <link linkend="ddl-periods-application-periods"><literal>PERIOD</literal></link>,
    you may supply a <literal>FOR PORTION OF</literal> clause, and your delete will
    only affect rows that overlap the given interval. Furthermore, if a row's span
    extends outside the <literal>FOR PORTION OF</literal> bounds, then your delete
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 71a02697f2d..a1b88571d0a 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -53,7 +53,8 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
   </para>
 
   <para>
-   If the table has a range or multirange column,
+   If the table has a range or multirange column, or a
+   <link linkend="ddl-periods-application-periods"><literal>PERIOD</literal></link>,
    you may supply a <literal>FOR PORTION OF</literal> clause, and your update will
    only affect rows that overlap the given interval. Furthermore, if a row's span
    extends outside the <literal>FOR PORTION OF</literal> bounds, then your update
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index d6b23b4bfd5..b3b519d9e4c 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -39,6 +39,7 @@ OBJS = \
 	pg_namespace.o \
 	pg_operator.o \
 	pg_parameter_acl.o \
+	pg_period.o \
 	pg_proc.o \
 	pg_publication.o \
 	pg_range.o \
@@ -111,6 +112,7 @@ CATALOG_HEADERS := \
 	pg_collation.h \
 	pg_parameter_acl.h \
 	pg_partitioned_table.h \
+	pg_period.h \
 	pg_range.h \
 	pg_transform.h \
 	pg_sequence.h \
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 590affb79a5..50ca706a160 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2817,6 +2817,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
+					case OBJECT_PERIOD:
 					case OBJECT_PUBLICATION_NAMESPACE:
 					case OBJECT_PUBLICATION_REL:
 					case OBJECT_ROLE:
@@ -2958,6 +2959,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
 					case OBJECT_PARAMETER_ACL:
+					case OBJECT_PERIOD:
 					case OBJECT_PUBLICATION_NAMESPACE:
 					case OBJECT_PUBLICATION_REL:
 					case OBJECT_ROLE:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e742c78ea35..3e1d2cbc692 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -48,6 +48,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_publication.h"
@@ -154,6 +155,7 @@ static const Oid object_classes[] = {
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
+	PeriodRelationId,			/* OCLASS_PERIOD */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
 	LanguageRelationId,			/* OCLASS_LANGUAGE */
@@ -1443,6 +1445,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveConstraintById(object->objectId);
 			break;
 
+		case OCLASS_PERIOD:
+			RemovePeriodById(object->objectId);
+			break;
+
 		case OCLASS_DEFAULT:
 			RemoveAttrDefaultById(object->objectId);
 			break;
@@ -2862,6 +2868,9 @@ getObjectClass(const ObjectAddress *object)
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 		case ConversionRelationId:
 			return OCLASS_CONVERSION;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c73f7bcd011..d02cf0da8fb 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_tablespace.h"
@@ -2040,6 +2041,81 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 	table_close(tablerel, AccessExclusiveLock);
 }
 
+/*
+ * Store a period of relation rel.
+ *
+ * Returns the OID of the new pg_period tuple.
+ */
+Oid
+StorePeriod(Relation rel, const char *periodname, AttrNumber startnum,
+			AttrNumber endnum, AttrNumber rangenum, Oid conoid)
+{
+	Datum		values[Natts_pg_period];
+	bool		nulls[Natts_pg_period];
+	Relation	pg_period;
+	HeapTuple	tuple;
+	Oid			oid;
+	NameData	pername;
+	ObjectAddress	myself, referenced;
+
+	Assert(rangenum != InvalidAttrNumber);
+
+	namestrcpy(&pername, periodname);
+
+	MemSet(values, 0, sizeof(values));
+	MemSet(nulls, false, sizeof(nulls));
+
+	pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+	oid = GetNewOidWithIndex(pg_period, AttrDefaultOidIndexId, Anum_pg_period_oid);
+	values[Anum_pg_period_oid - 1] = ObjectIdGetDatum(oid);
+	values[Anum_pg_period_pername - 1] = NameGetDatum(&pername);
+	values[Anum_pg_period_perrelid - 1] = RelationGetRelid(rel);
+	values[Anum_pg_period_perstart - 1] = startnum;
+	values[Anum_pg_period_perend - 1] = endnum;
+	values[Anum_pg_period_perrange - 1] = rangenum;
+	values[Anum_pg_period_perconstraint - 1] = conoid;
+
+	tuple = heap_form_tuple(RelationGetDescr(pg_period), values, nulls);
+	CatalogTupleInsert(pg_period, tuple);
+
+	ObjectAddressSet(myself, PeriodRelationId, oid);
+
+	/* Drop the period when the table is dropped. */
+	ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel));
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+
+	/* Forbid dropping the columns of the period. */
+	ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), startnum);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), endnum);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/*
+	 * The range column is an implementation detail,
+	 * but we can't use DEPENDENCY_INTERNAL
+	 * because dropping the table will check for dependencies on all subobjects too
+	 * (in findDependentObjects).
+	 * But if we make an AUTO dependency one way we will auto-drop the column
+	 * when we drop the PERIOD,
+	 * and a NORMAL dependency the other way we will forbid dropping the column directly.
+	 */
+	ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), rangenum);
+	recordDependencyOn(&referenced, &myself, DEPENDENCY_AUTO);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/*
+	 * The constraint is an implementation detail, so we mark it as such.
+	 * (Note that myself and referenced are reversed for this one.)
+	 */
+	ObjectAddressSet(referenced, ConstraintRelationId, conoid);
+	recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL);
+
+	table_close(pg_period, RowExclusiveLock);
+
+	return oid;
+}
+
 /*
  * Store a check-constraint expression for the given relation.
  *
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index c402ae72741..50ca0d05f87 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1207,7 +1207,28 @@ GRANT SELECT ON parameters TO PUBLIC;
  * PERIODS view
  */
 
--- feature not supported
+CREATE VIEW periods AS
+    SELECT current_database()::information_schema.sql_identifier AS table_catalog,
+           nc.nspname::information_schema.sql_identifier AS table_schema,
+           c.relname::information_schema.sql_identifier AS table_name,
+           p.pername::information_schema.sql_identifier AS period_name,
+           CASE WHEN pg_has_role(c.relowner, 'USAGE')
+                  OR has_column_privilege(sa.attrelid, sa.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+                THEN sa.attname::information_schema.sql_identifier
+           END AS start_column_name,
+           CASE WHEN pg_has_role(c.relowner, 'USAGE')
+                  OR has_column_privilege(ea.attrelid, ea.attnum, 'SELECT, INSERT, UPDATE, REFERENCES')
+                THEN ea.attname::information_schema.sql_identifier
+           END AS end_column_name
+    FROM pg_period AS p
+    JOIN pg_class AS c ON c.oid = p.perrelid
+    JOIN pg_namespace AS nc ON nc.oid = c.relnamespace
+    JOIN pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart)
+    JOIN pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend)
+    WHERE NOT pg_is_other_temp_schema(nc.oid)
+      AND c.relkind IN ('r', 'v');
+
+GRANT SELECT ON periods TO PUBLIC;

 /*
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 2f3ded8a0e7..a372fb0aae8 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -26,6 +26,7 @@ backend_sources += files(
   'pg_namespace.c',
   'pg_operator.c',
   'pg_parameter_acl.c',
+  'pg_period.c',
   'pg_proc.c',
   'pg_publication.c',
   'pg_range.c',
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8bb2924b9cd..8597b0cc2ae 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_publication.h"
@@ -736,6 +737,10 @@ static const struct object_type_map
 	{
 		"domain constraint", OBJECT_DOMCONSTRAINT
 	},
+	/* OCLASS_PERIOD */
+	{
+		"period", OBJECT_PERIOD
+	},
 	/* OCLASS_CONVERSION */
 	{
 		"conversion", OBJECT_CONVERSION
@@ -1014,6 +1019,7 @@ get_object_address(ObjectType objtype, Node *object,
 			case OBJECT_TRIGGER:
 			case OBJECT_TABCONSTRAINT:
 			case OBJECT_POLICY:
+			case OBJECT_PERIOD:
 				address = get_object_address_relobject(objtype, castNode(List, object),
 													   &relation, missing_ok);
 				break;
@@ -1512,6 +1518,13 @@ get_object_address_relobject(ObjectType objtype, List *object,
 				InvalidOid;
 			address.objectSubId = 0;
 			break;
+		case OBJECT_PERIOD:
+			address.classId = PeriodRelationId;
+			address.objectId = relation ?
+				get_relation_period_oid(reloid, depname, missing_ok) :
+				InvalidOid;
+			address.objectSubId = 0;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", (int) objtype);
 	}
@@ -2329,6 +2342,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_RULE:
 		case OBJECT_TRIGGER:
 		case OBJECT_TABCONSTRAINT:
+		case OBJECT_PERIOD:
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 			objnode = (Node *) name;
@@ -2439,6 +2453,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 		case OBJECT_TRIGGER:
 		case OBJECT_POLICY:
 		case OBJECT_TABCONSTRAINT:
+		case OBJECT_PERIOD:
 			if (!object_ownercheck(RelationRelationId, RelationGetRelid(relation), roleid))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   RelationGetRelationName(relation));
@@ -3087,6 +3102,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
+				perTup = SearchSysCache1(PERIODOID,
+										 ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(perTup))
+					elog(ERROR, "cache lookup failed for period %u",
+						 object->objectId);
+				per = (Form_pg_period) GETSTRUCT(perTup);
+
+				if (OidIsValid(per->perrelid))
+				{
+					StringInfoData rel;
+
+					initStringInfo(&rel);
+					getRelationDescription(&rel, per->perrelid, false);
+					appendStringInfo(&buffer, _("period %s on %s"),
+									 NameStr(per->pername), rel.data);
+					pfree(rel.data);
+				}
+				else
+				{
+					appendStringInfo(&buffer, _("period %s"),
+									 NameStr(per->pername));
+				}
+
+				ReleaseSysCache(perTup);
+				break;
+			}
+
 		case OCLASS_CONVERSION:
 			{
 				HeapTuple	conTup;
@@ -4451,6 +4498,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 										 missing_ok);
 			break;
 
+		case OCLASS_PERIOD:
+			appendStringInfoString(&buffer, "period");
+			break;
+
 		case OCLASS_CONVERSION:
 			appendStringInfoString(&buffer, "conversion");
 			break;
@@ -4958,6 +5009,28 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
+				perTup = SearchSysCache1(PERIODOID,
+										 ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(perTup))
+					elog(ERROR, "cache lookup failed for period %u",
+						 object->objectId);
+				per = (Form_pg_period) GETSTRUCT(perTup);
+
+				appendStringInfo(&buffer, "%s on ",
+								 quote_identifier(NameStr(per->pername)));
+				getRelationIdentity(&buffer, per->perrelid, objname, false);
+				if (objname)
+					*objname = lappend(*objname, pstrdup(NameStr(per->pername)));
+
+				ReleaseSysCache(perTup);
+				break;
+			}
+
 		case OCLASS_CONVERSION:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/catalog/pg_period.c b/src/backend/catalog/pg_period.c
new file mode 100644
index 00000000000..465b5b675e3
--- /dev/null
+++ b/src/backend/catalog/pg_period.c
@@ -0,0 +1,132 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.c
+ *	  routines to support manipulation of the pg_period relation
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/pg_period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_period.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+/*
+ * Delete a single period record.
+ */
+void
+RemovePeriodById(Oid periodId)
+{
+	Relation	pg_period;
+	HeapTuple	tup;
+
+	pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(PERIODOID, ObjectIdGetDatum(periodId));
+	if (!HeapTupleIsValid(tup)) /* should not happen */
+		elog(ERROR, "cache lookup failed for period %u", periodId);
+
+	/* Fry the period itself */
+	CatalogTupleDelete(pg_period, &tup->t_self);
+
+	/* Clean up */
+	ReleaseSysCache(tup);
+	table_close(pg_period, RowExclusiveLock);
+}
+
+/*
+ * get_relation_period_oid
+ *		Find a period on the specified relation with the specified name.
+ *		Returns period's OID.
+ */
+Oid
+get_relation_period_oid(Oid relid, const char *pername, bool missing_ok)
+{
+	Relation	pg_period;
+	HeapTuple	tuple;
+	SysScanDesc scan;
+	ScanKeyData skey[2];
+	Oid			perOid = InvalidOid;
+
+	/* Fetch the period tuple from pg_period. */
+	pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_period_perrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid));
+	ScanKeyInit(&skey[1],
+				Anum_pg_period_pername,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(pername));
+
+	scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+							  NULL, 2, skey);
+
+	/* There can be at most one matching row */
+	if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+		perOid = ((Form_pg_period) GETSTRUCT(tuple))->oid;
+
+	systable_endscan(scan);
+
+	/* If no such period exists, complain */
+	if (!OidIsValid(perOid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("period \"%s\" for table \"%s\" does not exist",
+						pername, get_rel_name(relid))));
+
+	table_close(pg_period, AccessShareLock);
+
+	return perOid;
+}
+
+/*
+ * get_period_attnos
+ *		Get the attno of the GENERATED rangetype column
+ *		for all PERIODs in this table.
+ */
+extern Bitmapset
+*get_period_attnos(Oid relid)
+{
+	Bitmapset *attnos = NULL;
+	Relation	pg_period;
+	HeapTuple	tuple;
+	SysScanDesc scan;
+	ScanKeyData skey[1];
+
+	pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_period_perrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relid));
+
+	scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+							  NULL, 1, skey);
+
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_period period = (Form_pg_period) GETSTRUCT(tuple);
+
+		attnos = bms_add_member(attnos, period->perrange);
+	}
+
+	systable_endscan(scan);
+	table_close(pg_period, AccessShareLock);
+
+	return attnos;
+}
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 80c40eaf578..2c1d1a10a7c 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -449,7 +449,7 @@ T176	Sequence generator support			NO	supported except for NEXT VALUE FOR
 T177	Sequence generator support: simple restart option			YES	
 T178	Identity columns: simple restart option			YES	
 T180	System-versioned tables			NO	
-T181	Application-time period tables			NO	
+T181	Application-time period tables			YES	
 T191	Referential action RESTRICT			YES	
 T200	Trigger DDL			NO	similar but not fully compatible
 T201	Comparable data types for referential constraints			YES	
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index b2e4260aef7..3467b68c0c6 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -669,6 +669,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c
index e9d50fc7d87..05fefb5b5ce 100644
--- a/src/backend/commands/comment.c
+++ b/src/backend/commands/comment.c
@@ -102,6 +102,16 @@ CommentObject(CommentStmt *stmt)
 								RelationGetRelationName(relation)),
 						 errdetail_relkind_not_supported(relation->rd_rel->relkind)));
 			break;
+
+		case OBJECT_PERIOD:
+			/* Periods can only go on tables */
+			if (relation->rd_rel->relkind != RELKIND_RELATION)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is not a table",
+								RelationGetRelationName(relation))));
+			break;
+
 		default:
 			break;
 	}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 87a2db4e0f5..4ae9921d3cb 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -509,6 +509,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_DOMCONSTRAINT:
 		case OBJECT_LARGEOBJECT:
 		case OBJECT_PARAMETER_ACL:
+		case OBJECT_PERIOD:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
 		case OBJECT_TABCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index b146d2aaf44..cf5aa5386a0 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1138,6 +1138,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PERIOD:
 		case OBJECT_POLICY:
 		case OBJECT_PROCEDURE:
 		case OBJECT_PUBLICATION:
@@ -1196,6 +1197,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -2237,6 +2239,7 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
+		case OBJECT_PERIOD:
 		case OBJECT_POLICY:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -2321,6 +2324,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_PARAMETER_ACL:
+		case OBJECT_PERIOD:
 		case OBJECT_POLICY:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_NAMESPACE:
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 5607273bf9f..af7177179ce 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -79,6 +79,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_PARAMETER_ACL:
+		case OBJECT_PERIOD:
 		case OBJECT_POLICY:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 54b4527aa77..b6451336a1c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -44,6 +44,7 @@
 #include "catalog/pg_largeobject.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
@@ -153,6 +154,11 @@ typedef enum AlterTablePass
 	AT_PASS_OLD_INDEX,			/* re-add existing indexes */
 	AT_PASS_OLD_CONSTR,			/* re-add existing constraints */
 	/* We could support a RENAME COLUMN pass here, but not currently used */
+	/*
+	 * We must add PERIODs after columns, in case they reference a newly-added column,
+	 * and before constraints, in case a newly-added PK/FK references them.
+	 */
+	AT_PASS_ADD_PERIOD,			/* ADD PERIOD */
 	AT_PASS_ADD_CONSTR,			/* ADD constraints (initial examination) */
 	AT_PASS_COL_ATTRS,			/* set column attributes, eg NOT NULL */
 	AT_PASS_ADD_INDEXCONSTR,	/* ADD index-based constraints */
@@ -362,6 +368,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
 static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
 							 bool is_partition, List **supconstr,
 							 List **supnotnulls);
+static List *MergePeriods(char *relname, List *periods, List *tableElts, List *supers);
 static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
 static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
 static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
@@ -443,6 +450,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
 									 AlterTableUtilityContext *context);
 static bool check_for_column_name_collision(Relation rel, const char *colname,
 											bool if_not_exists);
+static bool check_for_period_name_collision(Relation rel, const char *pername,
+											bool if_not_exists);
 static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
 static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid);
 static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
@@ -462,6 +471,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
 										 Node *newDefault, LOCKMODE lockmode);
 static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 											   Node *newDefault);
+static ObjectAddress ATExecAddPeriod(Relation rel, PeriodDef *period,
+									 AlterTableUtilityContext *context);
+static void ATExecDropPeriod(Relation rel, const char *periodName,
+							 DropBehavior behavior,
+							 bool recurse, bool recursing,
+							 bool missing_ok);
 static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName,
 									   Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
 static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
@@ -675,6 +690,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void AddRelationNewPeriod(Relation rel, PeriodDef *period);
+static void ValidatePeriod(Relation rel, PeriodDef *period);
+static Constraint *make_constraint_for_period(Relation rel, PeriodDef *period);
+static ColumnDef *make_range_column_for_period(PeriodDef *period);

 /* ----------------------------------------------------------------
@@ -903,6 +922,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
+	 * Using the column list (including inherited columns),
+	 * find the start/end columns for each period.
+	 * PERIODs should be inherited too (but aren't yet).
+	 */
+	stmt->periods = MergePeriods(relname, stmt->periods, stmt->tableElts, inheritOids);
+
+	/*
+	 * For each PERIOD we need to create a GENERATED column,
+	 * so add those to tableElts.
+	 * These columns are not inherited,
+	 * so we don't worry about conflicts in tableElts.
+	 */
+	foreach(listptr, stmt->periods)
+	{
+		PeriodDef *period = (PeriodDef *) lfirst(listptr);
+		ColumnDef *col = make_range_column_for_period(period);
+		stmt->tableElts = lappend(stmt->tableElts, col);
+	}
+
 	/*
 	 * Create a tuple descriptor from the relation schema.  Note that this
 	 * deals with column names, types, and in-descriptor NOT NULL flags, but
@@ -1288,6 +1327,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	foreach(listptr, nncols)
 		set_attnotnull(NULL, rel, lfirst_int(listptr), false, NoLock);
 
+	/*
+	 * Create periods for the table. This must come after we create columns
+	 * and before we create index constraints. It will automatically create
+	 * a CHECK constraint for the period.
+	 */
+	foreach(listptr, stmt->periods)
+	{
+		PeriodDef *period = (PeriodDef *) lfirst(listptr);
+
+		/* Don't update the count of check constraints twice */
+		CommandCounterIncrement();
+
+		AddRelationNewPeriod(rel, period);
+	}
+
 	ObjectAddressSet(address, RelationRelationId, relationId);
 
 	/*
@@ -1404,6 +1458,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
+ * make_constraint_for_period
+ *
+ * Builds a CHECK Constraint to ensure start < end.
+ * Returns the CHECK Constraint.
+ * Also fills in period->constraintname if needed.
+ */
+static Constraint *
+make_constraint_for_period(Relation rel, PeriodDef *period)
+{
+	ColumnRef  *scol, *ecol;
+	Constraint *constr;
+	TypeCacheEntry *type;
+
+	if (period->constraintname == NULL)
+		period->constraintname = ChooseConstraintName(RelationGetRelationName(rel),
+													  period->periodname,
+													  "check",
+													  RelationGetNamespace(rel),
+													  NIL);
+	scol = makeNode(ColumnRef);
+	scol->fields = list_make1(makeString(pstrdup(period->startcolname)));
+	scol->location = 0;
+
+	ecol = makeNode(ColumnRef);
+	ecol->fields = list_make1(makeString(pstrdup(period->endcolname)));
+	ecol->location = 0;
+
+	type = lookup_type_cache(period->coltypid, TYPECACHE_LT_OPR);
+	if (type->lt_opr == InvalidOid)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("column \"%s\" cannot be used in a PERIOD because its type %s has no less than operator",
+						period->startcolname, format_type_be(period->coltypid))));
+
+	constr = makeNode(Constraint);
+	constr->contype = CONSTR_CHECK;
+	constr->conname = period->constraintname;
+	constr->deferrable = false;
+	constr->initdeferred = false;
+	constr->location = -1;
+	constr->is_no_inherit = false;
+	constr->raw_expr = (Node *) makeSimpleA_Expr(AEXPR_OP,
+												 get_opname(type->lt_opr),
+												 (Node *) scol,
+												 (Node *) ecol,
+												 0);
+	constr->cooked_expr = NULL;
+	constr->skip_validation = false;
+	constr->initially_valid = true;
+
+	return constr;
+}
+
+/*
+ * make_range_column_for_period
+ *
+ * Builds a GENERATED ALWAYS range column based on the PERIOD
+ * start/end columns. Returns the ColumnDef.
+ */
+ColumnDef *
+make_range_column_for_period(PeriodDef *period)
+{
+	char *range_type_namespace;
+	char *range_type_name;
+	ColumnDef *col = makeNode(ColumnDef);
+	ColumnRef *startvar, *endvar;
+	Expr *rangeConstructor;
+
+	if (!get_typname_and_namespace(period->rngtypid, &range_type_name, &range_type_namespace))
+		elog(ERROR, "missing range type %d", period->rngtypid);
+
+	startvar = makeNode(ColumnRef);
+	startvar->fields = list_make1(makeString(pstrdup(period->startcolname)));
+	endvar = makeNode(ColumnRef);
+	endvar->fields = list_make1(makeString(pstrdup(period->endcolname)));
+	rangeConstructor = (Expr *) makeFuncCall(
+			list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+			list_make2(startvar, endvar),
+			COERCE_EXPLICIT_CALL,
+			period->location);
+
+	col->colname = pstrdup(period->periodname);
+	col->typeName = makeTypeName(range_type_name);
+	col->compression = NULL;
+	col->inhcount = 0;
+	col->is_local = true;
+	col->is_not_null = true;
+	col->is_from_type = false;
+	col->storage = 0;
+	col->storage_name = NULL;
+	col->raw_default = (Node *) rangeConstructor;
+	col->cooked_default = NULL;
+	col->identity = 0;
+	col->generated = ATTRIBUTE_GENERATED_STORED;
+	col->collClause = NULL;
+	col->collOid = InvalidOid;
+	col->fdwoptions = NIL;
+	col->location = period->location;
+
+	return col;
+}
+
+/*
+ * ValidatePeriod
+ *
+ * Look up the attributes used by the PERIOD,
+ * make sure they exist, are not system columns,
+ * and have the same type and collation.
+ *
+ * You must have a RowExclusiveLock on pg_attribute
+ * before calling this function.
+ *
+ * Add our findings to these PeriodDef fields:
+ *
+ * coltypid - the type of PERIOD columns.
+ * startattnum - the attnum of the start column.
+ * endattnum - the attnum of the end column.
+ */
+static void
+ValidatePeriod(Relation rel, PeriodDef *period)
+{
+	HeapTuple starttuple;
+	HeapTuple endtuple;
+	Form_pg_attribute	atttuple;
+	Oid	attcollation;
+
+	/* Find the start column */
+	starttuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->startcolname);
+	if (!HeapTupleIsValid(starttuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						period->startcolname, RelationGetRelationName(rel))));
+	atttuple = (Form_pg_attribute) GETSTRUCT(starttuple);
+	period->coltypid = atttuple->atttypid;
+	attcollation = atttuple->attcollation;
+	period->startattnum = atttuple->attnum;
+
+	/* Make sure it's not a system column */
+	if (period->startattnum <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("cannot use system column \"%s\" in period",
+						period->startcolname)));
+
+	/* Find the end column */
+	endtuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->endcolname);
+	if (!HeapTupleIsValid(endtuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						period->endcolname, RelationGetRelationName(rel))));
+	atttuple = (Form_pg_attribute) GETSTRUCT(endtuple);
+	period->endattnum = atttuple->attnum;
+
+	/* Make sure it's not a system column */
+	if (period->endattnum <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("cannot use system column \"%s\" in period",
+						period->endcolname)));
+
+	/* Both columns must be of same type */
+	if (period->coltypid != atttuple->atttypid)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("start and end columns of period must be of same type")));
+
+	/* Both columns must have the same collation */
+	if (attcollation != atttuple->attcollation)
+		ereport(ERROR,
+				(errcode(ERRCODE_COLLATION_MISMATCH),
+				 errmsg("start and end columns of period must have same collation")));
+
+	heap_freetuple(starttuple);
+	heap_freetuple(endtuple);
+}
+
+/*
+ * choose_rangetype_for_period
+ *
+ * Find a suitable range type for operations involving this period.
+ * Use the rangetype option if provided, otherwise try to find a
+ * non-ambiguous existing type.
+ */
+// TODO: This could be static now
+Oid
+choose_rangetype_for_period(PeriodDef *period)
+{
+	Oid	rngtypid;
+
+	if (period->rangetypename != NULL)
+	{
+		/* Make sure it exists */
+		rngtypid = TypenameGetTypidExtended(period->rangetypename, false);
+		if (rngtypid == InvalidOid)
+			ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("Range type %s not found", period->rangetypename)));
+
+		/* Make sure it is a range type */
+		if (!type_is_range(rngtypid))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("Type %s is not a range type", period->rangetypename)));
+
+		/* Make sure it matches the column type */
+		if (get_range_subtype(rngtypid) != period->coltypid)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("Range type %s does not match column type %s",
+						 period->rangetypename,
+						 format_type_be(period->coltypid))));
+	}
+	else
+	{
+		rngtypid = get_subtype_range(period->coltypid);
+		if (rngtypid == InvalidOid)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("no range type for %s found for period %s",
+							format_type_be(period->coltypid),
+							period->periodname),
+					 errhint("You can define a custom range type with CREATE TYPE")));
+
+	}
+
+	return rngtypid;
+}
+
+static void
+AddRelationNewPeriod(Relation rel, PeriodDef *period)
+{
+	Relation	attrelation;
+	Oid			conoid;
+	Constraint *constr;
+	List	   *newconstrs;
+
+	attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+
+	/* Find the GENERATED range column */
+
+	period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+	if (period->rngattnum == InvalidAttrNumber)
+		elog(ERROR, "missing attribute %s", period->periodname);
+
+	/* The parser has already found period->coltypid */
+
+	constr = make_constraint_for_period(rel, period);
+	newconstrs = AddRelationNewConstraints(rel, NIL, list_make1(constr), false, true, true, NULL);
+	conoid = ((CookedConstraint *) linitial(newconstrs))->conoid;
+
+	/* Save it */
+	StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+	table_close(attrelation, RowExclusiveLock);
+}
+
 /*
  * Emit the right error or warning message for a "DROP" command issued on a
  * non-existent relation
@@ -3369,6 +3681,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
+ *		Returns new period list given initial periods and superclasses.
+ *
+ * For now we don't support inheritence with PERIODs,
+ * but we might make it work eventually.
+ *
+ * We can omit lots of checks here and assume MergeAttributes already did them,
+ * for example that child & parents are not a mix of permanent and temp.
+ */
+static List *
+MergePeriods(char *relname, List *periods, List *tableElts, List *supers)
+{
+	ListCell   *entry;
+
+	/* If we have a PERIOD then supers must be empty. */
+
+	if (list_length(periods) > 0 && list_length(supers) > 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("Inheriting is not supported when a table has a PERIOD")));
+
+	/* If any parent table has a PERIOD, then fail. */
+
+	foreach(entry, supers)
+	{
+		Oid			parent = lfirst_oid(entry);
+		Relation	relation;
+		Relation	pg_period;
+		SysScanDesc scan;
+		ScanKeyData skey[1];
+		HeapTuple	tuple;
+
+		/* caller already got lock */
+		relation = table_open(parent, NoLock);
+		pg_period = table_open(PeriodRelationId, AccessShareLock);
+
+		ScanKeyInit(&skey[0],
+					Anum_pg_period_perrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(parent));
+
+		scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true,
+								  NULL, 1, skey);
+
+		if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("Inheriting from a table with a PERIOD is not supported")));
+
+		systable_endscan(scan);
+		table_close(pg_period, AccessShareLock);
+		table_close(relation, NoLock);
+	}
+
+	/*
+	 * Find the start & end columns and get their attno and type.
+	 * In the same pass, make sure the period doesn't conflict with any column names.
+	 * Also make sure the same period name isn't used more than once.
+	 */
+	foreach (entry, periods)
+	{
+		PeriodDef *period = lfirst(entry);
+		ListCell *entry2;
+		int i = 1;
+		Oid startcoltypid = InvalidOid;
+		Oid endcoltypid = InvalidOid;
+		Oid	startcolcollation = InvalidOid;
+		Oid	endcolcollation = InvalidOid;
+
+		period->startattnum = InvalidAttrNumber;
+		period->endattnum = InvalidAttrNumber;
+
+		if (SystemAttributeByName(period->periodname) != NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_COLUMN),
+					 errmsg("period name \"%s\" conflicts with a system column name",
+							period->periodname)));
+
+		foreach (entry2, periods)
+		{
+			PeriodDef *period2 = lfirst(entry2);
+
+			if (period != period2 && strcmp(period->periodname, period2->periodname) == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_OBJECT),
+						 errmsg("period name \"%s\" specified more than once",
+								period->periodname)));
+		}
+
+		foreach (entry2, tableElts)
+		{
+			ColumnDef *col = lfirst(entry2);
+			int32	atttypmod;
+			AclResult aclresult;
+
+			if (strcmp(period->periodname, col->colname) == 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_DUPLICATE_COLUMN),
+						 errmsg("period name \"%s\" conflicts with a column name",
+								period->periodname)));
+
+			if (strcmp(period->startcolname, col->colname) == 0)
+			{
+				period->startattnum = i;
+
+				typenameTypeIdAndMod(NULL, col->typeName, &startcoltypid, &atttypmod);
+
+				aclresult = object_aclcheck(TypeRelationId, startcoltypid, GetUserId(), ACL_USAGE);
+				if (aclresult != ACLCHECK_OK)
+					aclcheck_error_type(aclresult, startcoltypid);
+
+				startcolcollation = GetColumnDefCollation(NULL, col, startcoltypid);
+			}
+
+			if (strcmp(period->endcolname, col->colname) == 0)
+			{
+				period->endattnum = i;
+
+				typenameTypeIdAndMod(NULL, col->typeName, &endcoltypid, &atttypmod);
+
+				aclresult = object_aclcheck(TypeRelationId, endcoltypid, GetUserId(), ACL_USAGE);
+				if (aclresult != ACLCHECK_OK)
+					aclcheck_error_type(aclresult, endcoltypid);
+
+				endcolcollation = GetColumnDefCollation(NULL, col, endcoltypid);
+			}
+
+			i++;
+		}
+
+		/* Did we find the columns? */
+		if (period->startattnum == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							period->startcolname, relname)));
+		if (period->endattnum == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							period->endcolname, relname)));
+
+		/* Both columns must be of same type */
+		if (startcoltypid != endcoltypid)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("start and end columns of period must be of same type")));
+
+		/* Both columns must have the same collation */
+		if (startcolcollation != endcolcollation)
+			ereport(ERROR,
+					(errcode(ERRCODE_COLLATION_MISMATCH),
+					 errmsg("start and end columns of period must have same collation")));
+
+		period->coltypid = startcoltypid;
+		period->rngtypid = choose_rangetype_for_period(period);
+	}
+
+	return periods;
+}
+
 /*
  * MergeCheckConstraint
  *		Try to merge an inherited CHECK constraint with previous ones
@@ -4425,12 +4899,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
  * existing query plans.  On the assumption it's not used for such, we
  * don't have to reject pending AFTER triggers, either.
  *
- * Also, since we don't have an AlterTableUtilityContext, this cannot be
+ * Also, if you don't pass an AlterTableUtilityContext, this cannot be
  * used for any subcommand types that require parse transformation or
  * could generate subcommands that have to be passed to ProcessUtility.
  */
 void
-AlterTableInternal(Oid relid, List *cmds, bool recurse)
+AlterTableInternal(Oid relid, List *cmds, bool recurse, AlterTableUtilityContext *context)
 {
 	Relation	rel;
 	LOCKMODE	lockmode = AlterTableGetLockLevel(cmds);
@@ -4439,7 +4913,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)
 
 	EventTriggerAlterTableRelid(relid);
 
-	ATController(NULL, rel, cmds, recurse, lockmode, NULL);
+	ATController(NULL, rel, cmds, recurse, lockmode, context);
 }
 
 /*
@@ -4532,6 +5006,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
 			case AT_EnableRule: /* may change SELECT rules */
 			case AT_DisableRule:	/* may change SELECT rules */
+			case AT_AddPeriod: /* shares namespace with columns, adds constraint */
+			case AT_DropPeriod:
 				cmd_lockmode = AccessExclusiveLock;
 				break;
 
@@ -4848,6 +5324,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* This command never recurses */
 			pass = AT_PASS_ADD_OTHERCONSTR;
 			break;
+		case AT_AddPeriod: /* ALTER TABLE ... ADD PERIOD FOR name (start, end) */
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			pass = AT_PASS_ADD_PERIOD;
+			break;
+		case AT_DropPeriod: /* ALTER TABLE ... DROP PERIOD FOR name */
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			pass = AT_PASS_DROP;
+			break;
 		case AT_AddIdentity:
 			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE);
 			/* Set up recursion for phase 2; no other prep needed */
@@ -5255,6 +5739,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_CookedColumnDefault:	/* add a pre-cooked default */
 			address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def);
 			break;
+		case AT_AddPeriod:
+			address = ATExecAddPeriod(rel, (PeriodDef *) cmd->def, context);
+			break;
+		case AT_DropPeriod:
+			ATExecDropPeriod(rel, cmd->name, cmd->behavior,
+								 false, false,
+								 cmd->missing_ok);
+			break;
 		case AT_AddIdentity:
 			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
 									  cur_pass, context);
@@ -6402,6 +6894,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 		case AT_AddColumn:
 		case AT_AddColumnToView:
 			return "ADD COLUMN";
+		case AT_AddPeriod:
+			return "ADD PERIOD";
 		case AT_ColumnDefault:
 		case AT_CookedColumnDefault:
 			return "ALTER COLUMN ... SET DEFAULT";
@@ -6427,6 +6921,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "ALTER COLUMN ... SET COMPRESSION";
 		case AT_DropColumn:
 			return "DROP COLUMN";
+		case AT_DropPeriod:
+			return "DROP PERIOD";
 		case AT_AddIndex:
 		case AT_ReAddIndex:
 			return NULL;		/* not real grammar */
@@ -7393,14 +7889,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 /*
  * If a new or renamed column will collide with the name of an existing
  * column and if_not_exists is false then error out, else do nothing.
+ *
+ * See also check_for_period_name_collision.
  */
 static bool
 check_for_column_name_collision(Relation rel, const char *colname,
 								bool if_not_exists)
 {
-	HeapTuple	attTuple;
+	HeapTuple	attTuple, perTuple;
 	int			attnum;
 
+	/* If the name exists as a period, we're done. */
+	perTuple = SearchSysCache2(PERIODNAME,
+							   ObjectIdGetDatum(RelationGetRelid(rel)),
+							   PointerGetDatum(colname));
+	if (HeapTupleIsValid(perTuple))
+	{
+		ReleaseSysCache(perTuple);
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_COLUMN),
+				 errmsg("column name \"%s\" conflicts with a period name",
+						colname)));
+	}
+
 	/*
 	 * this test is deliberately not attisdropped-aware, since if one tries to
 	 * add a column matching a dropped column name, it's gonna fail anyway.
@@ -7444,6 +7955,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
+ * If a new period name will collide with the name of an existing column or
+ * period [and if_not_exists is false] then error out, else do nothing.
+ *
+ * See also check_for_column_name_collision.
+ */
+static bool
+check_for_period_name_collision(Relation rel, const char *pername,
+								bool if_not_exists)
+{
+	HeapTuple	attTuple, perTuple;
+	int			attnum;
+
+	/* TODO: implement IF [NOT] EXISTS for periods */
+	Assert(!if_not_exists);
+
+	/* If there is already a period with this name, then we're done. */
+	perTuple = SearchSysCache2(PERIODNAME,
+							   ObjectIdGetDatum(RelationGetRelid(rel)),
+							   PointerGetDatum(pername));
+	if (HeapTupleIsValid(perTuple))
+	{
+		if (if_not_exists)
+		{
+			ReleaseSysCache(perTuple);
+
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_COLUMN),
+					 errmsg("period \"%s\" of relation \"%s\" already exists, skipping",
+							pername, RelationGetRelationName(rel))));
+			return false;
+		}
+
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_COLUMN),
+				 errmsg("period \"%s\" of relation \"%s\" already exists",
+						pername, RelationGetRelationName(rel))));
+	}
+
+	/*
+	 * this test is deliberately not attisdropped-aware, since if one tries to
+	 * add a column matching a dropped column name, it's gonna fail anyway.
+	 */
+	attTuple = SearchSysCache2(ATTNAME,
+							   ObjectIdGetDatum(RelationGetRelid(rel)),
+							   PointerGetDatum(pername));
+	if (HeapTupleIsValid(attTuple))
+	{
+		attnum = ((Form_pg_attribute) GETSTRUCT(attTuple))->attnum;
+		ReleaseSysCache(attTuple);
+
+		/*
+		 * We throw a different error message for conflicts with system column
+		 * names, since they are normally not shown and the user might otherwise
+		 * be confused about the reason for the conflict.
+		 */
+		if (attnum <= 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_COLUMN),
+					 errmsg("period name \"%s\" conflicts with a system column name",
+							pername)));
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_COLUMN),
+				 errmsg("period name \"%s\" conflicts with a column name",
+						pername)));
+	}
+
+	return true;
+}
+
 /*
  * Install a column's dependency on its datatype.
  */
@@ -8095,6 +8676,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
+ * ALTER TABLE ADD PERIOD
+ *
+ * Return the address of the period.
+ */
+static ObjectAddress
+ATExecAddPeriod(Relation rel, PeriodDef *period, AlterTableUtilityContext *context)
+{
+	Relation		attrelation;
+	ObjectAddress	address = InvalidObjectAddress;
+	Constraint	   *constr;
+	ColumnDef	   *rangecol;
+	Oid				conoid, periodoid;
+	List		   *cmds = NIL;
+	AlterTableCmd  *cmd;
+
+	/*
+	 * PERIOD FOR SYSTEM_TIME is not yet implemented, but make sure no one uses
+	 * the name.
+	 */
+	if (strcmp(period->periodname, "system_time") == 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("PERIOD FOR SYSTEM_TIME is not supported")));
+
+	/* The period name must not already exist */
+	(void) check_for_period_name_collision(rel, period->periodname, false);
+
+	/* Parse options */
+	transformPeriodOptions(period);
+
+	attrelation = table_open(AttributeRelationId, RowExclusiveLock);
+	ValidatePeriod(rel, period);
+
+	period->rngtypid = choose_rangetype_for_period(period);
+
+	/* Make the CHECK constraint */
+	constr = make_constraint_for_period(rel, period);
+	cmd = makeNode(AlterTableCmd);
+	cmd->subtype = AT_AddConstraint;
+	cmd->def = (Node *) constr;
+	cmds = lappend(cmds, cmd);
+	AlterTableInternal(RelationGetRelid(rel), cmds, true, context);
+	conoid = get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false);
+
+	/* Make the range column */
+	rangecol = make_range_column_for_period(period);
+	cmd = makeNode(AlterTableCmd);
+	cmd->subtype = AT_AddColumn;
+	cmd->def = (Node *) rangecol;
+	cmd->name = period->periodname;
+	cmd->recurse = false; /* no, let the PERIOD recurse instead */
+	AlterTableInternal(RelationGetRelid(rel), list_make1(cmd), true, context);
+	period->rngattnum = get_attnum(RelationGetRelid(rel), period->periodname);
+	if (period->rngattnum == InvalidAttrNumber)
+		elog(ERROR, "missing attribute %s", period->periodname);
+
+	/* Save the Period */
+	periodoid = StorePeriod(rel, period->periodname, period->startattnum, period->endattnum, period->rngattnum, conoid);
+
+	ObjectAddressSet(address, PeriodRelationId, periodoid);
+
+	table_close(attrelation, RowExclusiveLock);
+
+	return address;
+}
+
+/*
+ * ALTER TABLE DROP PERIOD
+ *
+ * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism.
+ */
+static void
+ATExecDropPeriod(Relation rel, const char *periodName,
+					 DropBehavior behavior,
+					 bool recurse, bool recursing,
+					 bool missing_ok)
+{
+	Relation	pg_period;
+	Form_pg_period period;
+	SysScanDesc scan;
+	ScanKeyData key;
+	HeapTuple	tuple;
+	bool		found = false;
+
+	/* At top level, permission check was done in ATPrepCmd, else do it */
+	if (recursing)
+		ATSimplePermissions(AT_DropPeriod, rel, ATT_TABLE);
+
+	pg_period = table_open(PeriodRelationId, RowExclusiveLock);
+
+	/*
+	 * Find and drop the target period
+	 */
+	ScanKeyInit(&key,
+				Anum_pg_period_perrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(rel)));
+	scan = systable_beginscan(pg_period, PeriodRelidNameIndexId,
+							  true, NULL, 1, &key);
+
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		ObjectAddress perobj;
+
+		period = (Form_pg_period) GETSTRUCT(tuple);
+
+		if (strcmp(NameStr(period->pername), periodName) != 0)
+			continue;
+
+		/*
+		 * Perform the actual period deletion
+		 */
+		perobj.classId = PeriodRelationId;
+		perobj.objectId = period->oid;
+		perobj.objectSubId = 0;
+
+		performDeletion(&perobj, behavior, 0);
+
+		found = true;
+
+		/* period found and dropped -- no need to keep looping */
+		break;
+	}
+
+	systable_endscan(scan);
+
+	if (!found)
+	{
+		if (!missing_ok)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("period \"%s\" on relation \"%s\" does not exist",
+							periodName, RelationGetRelationName(rel))));
+		}
+		else
+		{
+			ereport(NOTICE,
+					(errmsg("period \"%s\" on relation \"%s\" does not exist, skipping",
+							periodName, RelationGetRelationName(rel))));
+			table_close(pg_period, RowExclusiveLock);
+			return;
+		}
+	}
+
+	table_close(pg_period, RowExclusiveLock);
+}
+
 /*
  * ALTER TABLE ALTER COLUMN ADD IDENTITY
  *
@@ -14166,6 +14896,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used by a period"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject, false),
+								   colName)));
+				break;
+
 			case OCLASS_REWRITE:
 				/* XXX someday see if we can cope with revising views */
 				if (subtype == AT_AlterColumnType)
@@ -16042,7 +16781,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)
 
 		EventTriggerAlterTableStart((Node *) stmt);
 		/* OID is set by AlterTableInternal */
-		AlterTableInternal(lfirst_oid(l), cmds, false);
+		AlterTableInternal(lfirst_oid(l), cmds, false, NULL);
 		EventTriggerAlterTableEnd();
 	}
 
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index dce898c751c..54bdb4bdeaf 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -169,7 +169,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			}
 
 			/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
-			AlterTableInternal(viewOid, atcmds, true);
+			AlterTableInternal(viewOid, atcmds, true, NULL);
 
 			/* Make the new view columns visible */
 			CommandCounterIncrement();
@@ -201,7 +201,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		atcmds = list_make1(atcmd);
 
 		/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
-		AlterTableInternal(viewOid, atcmds, true);
+		AlterTableInternal(viewOid, atcmds, true, NULL);
 
 		/*
 		 * There is very little to do here to update the view's dependencies.
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 6c19844e17e..676ed85d203 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1639,6 +1639,9 @@ exprLocation(const Node *expr)
 		case T_Constraint:
 			loc = ((const Constraint *) expr)->location;
 			break;
+		case T_PeriodDef:
+			loc = ((const PeriodDef *) expr)->location;
+			break;
 		case T_FunctionParameter:
 			/* just use typename's location */
 			loc = exprLocation((Node *) ((const FunctionParameter *) expr)->argType);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c85b8b094ec..a0a6f5681e4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -595,7 +595,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
 
-%type <node>	TableConstraint TableLikeClause
+%type <node>	TableConstraint TableLikeClause TablePeriod
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
 %type <list>	ColQualList
@@ -2625,6 +2625,24 @@ alter_table_cmd:
 					n->def = (Node *) $4;
 					$$ = (Node *) n;
 				}
+			/* ALTER TABLE <name> ADD PERIOD FOR <name> (<name>, <name>) */
+			| ADD_P TablePeriod
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_AddPeriod;
+					n->def = $2;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> DROP PERIOD FOR <name> [RESTRICT|CASCADE] */
+			| DROP PERIOD FOR name opt_drop_behavior
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_DropPeriod;
+					n->name = $4;
+					n->behavior = $5;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			/* ALTER TABLE <name> ADD CONSTRAINT ... */
 			| ADD_P TableConstraint
 				{
@@ -3749,8 +3767,10 @@ TableElement:
 			columnDef							{ $$ = $1; }
 			| TableLikeClause					{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
+			| TablePeriod						{ $$ = $1; }
 		;
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4102,6 +4122,19 @@ TableLikeOption:
 		;

+TablePeriod:
+			PERIOD FOR name '(' name ',' name ')' opt_definition
+				{
+					PeriodDef *n = makeNode(PeriodDef);
+					n->periodname = $3;
+					n->startcolname = $5;
+					n->endcolname = $7;
+					n->options = $9;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
+		;
+
 /* ConstraintElem specifies constraint syntax which is not embedded into
  *	a column definition. ColConstraintElem specifies the embedded form.
  * - thomas 1997-12-03
@@ -7160,6 +7193,14 @@ CommentStmt:
 					n->comment = $9;
 					$$ = (Node *) n;
 				}
+			| COMMENT ON PERIOD any_name IS comment_text
+				{
+					CommentStmt *n = makeNode(CommentStmt);
+					n->objtype = OBJECT_PERIOD;
+					n->object = (Node *) $4;
+					n->comment = $6;
+					$$ = (Node *) n;
+				}
 			| COMMENT ON LARGE_P OBJECT_P NumericOnly IS comment_text
 				{
 					CommentStmt *n = makeNode(CommentStmt);
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 34a0ec59019..17e85a7c83b 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -22,6 +22,7 @@
 #include "access/table.h"
 #include "catalog/heap.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "nodes/makefuncs.h"
@@ -3189,6 +3190,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
 				  int sublevels_up, bool require_col_privs, int location)
 {
 	RangeTblEntry *rte = nsitem->p_rte;
+	Bitmapset  *periodatts = NULL;
 	RTEPermissionInfo *perminfo = nsitem->p_perminfo;
 	List	   *names,
 			   *vars;
@@ -3212,12 +3214,20 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
 		perminfo->requiredPerms |= ACL_SELECT;
 	}
 
+	/* Get PERIOD columns to exclude */
+	if (rte->rtekind == RTE_RELATION)
+		periodatts = get_period_attnos(rte->relid);
+
 	forboth(name, names, var, vars)
 	{
 		char	   *label = strVal(lfirst(name));
 		Var		   *varnode = (Var *) lfirst(var);
 		TargetEntry *te;
 
+		/* If this column is from a PERIOD, skip it */
+		if (bms_is_member(pstate->p_next_resno, periodatts))
+				continue;
+
 		te = makeTargetEntry((Expr *) varnode,
 							 (AttrNumber) pstate->p_next_resno++,
 							 label,
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 0772a0210e7..84df1d32192 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -37,6 +37,7 @@
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
@@ -80,6 +81,7 @@ typedef struct
 	bool		isforeign;		/* true if CREATE/ALTER FOREIGN TABLE */
 	bool		isalter;		/* true if altering existing table */
 	List	   *columns;		/* ColumnDef items */
+	List	   *periods;		/* PeriodDef items */
 	List	   *ckconstraints;	/* CHECK constraints */
 	List	   *nnconstraints;	/* NOT NULL constraints */
 	List	   *fkconstraints;	/* FOREIGN KEY constraints */
@@ -111,6 +113,8 @@ typedef struct
 
 static void transformColumnDefinition(CreateStmtContext *cxt,
 									  ColumnDef *column);
+static void transformTablePeriod(CreateStmtContext *cxt,
+								 PeriodDef *period);
 static void transformTableConstraint(CreateStmtContext *cxt,
 									 Constraint *constraint);
 static void transformTableLikeClause(CreateStmtContext *cxt,
@@ -240,6 +244,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	cxt.inhRelations = stmt->inhRelations;
 	cxt.isalter = false;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.nnconstraints = NIL;
 	cxt.fkconstraints = NIL;
@@ -280,6 +285,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 				transformColumnDefinition(&cxt, (ColumnDef *) element);
 				break;
 
+			case T_PeriodDef:
+				transformTablePeriod(&cxt, (PeriodDef *) element);
+				break;
+
 			case T_Constraint:
 				transformTableConstraint(&cxt, (Constraint *) element);
 				break;
@@ -347,6 +356,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	 * Output results.
 	 */
 	stmt->tableElts = cxt.columns;
+	stmt->periods = cxt.periods;
 	stmt->constraints = cxt.ckconstraints;
 	stmt->nnconstraints = cxt.nnconstraints;
 
@@ -913,6 +923,113 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
+transformPeriodOptions(PeriodDef *period)
+{
+	ListCell   *option;
+	DefElem	   *dconstraintname = NULL;
+	DefElem	   *drangetypename = NULL;
+
+	foreach(option, period->options)
+	{
+		DefElem    *defel = (DefElem *) lfirst(option);
+
+		if (strcmp(defel->defname, "check_constraint_name") == 0)
+		{
+			if (dconstraintname)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			dconstraintname = defel;
+		}
+		else if (strcmp(defel->defname, "rangetype") == 0)
+		{
+			if (drangetypename)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("conflicting or redundant options")));
+			drangetypename = defel;
+		}
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("option \"%s\" not recognized", defel->defname)));
+	}
+
+	if (dconstraintname != NULL)
+		period->constraintname = defGetString(dconstraintname);
+	else
+		period->constraintname = NULL;
+
+	if (drangetypename != NULL)
+		period->rangetypename = defGetString(drangetypename);
+	else
+		period->rangetypename = NULL;
+}
+
+/*
+ * transformTablePeriod
+ *		transform a PeriodDef node within CREATE TABLE or ALTER TABLE
+ *		TODO: Does ALTER TABLE really call us?? It doesn't seem like it does. Maybe it should. Did it in Vik's original patch?
+ */
+static void
+transformTablePeriod(CreateStmtContext *cxt, PeriodDef *period)
+{
+	// Oid			coltypid;
+	// ColumnDef  *col;
+	// ListCell   *columns;
+
+	if (strcmp(period->periodname, "system_time") == 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("PERIOD FOR SYSTEM_TIME is not supported"),
+					 parser_errposition(cxt->pstate,
+										period->location)));
+
+	if (strcmp(period->startcolname, period->endcolname) == 0)
+		ereport(ERROR, (errmsg("column \"%s\" can't be the start and end column for period \"%s\"",
+						period->startcolname, period->periodname)));
+
+	/*
+	 * Determine the column info and range type so that transformIndexConstraints
+	 * knows how to create PRIMARY KEY/UNIQUE constraints using this PERIOD.
+	 */
+	transformPeriodOptions(period);
+
+	/*
+	 * Find a suitable range type for operations involving this period.
+	 * Use the rangetype option if provided, otherwise try to find a
+	 * non-ambiguous existing type.
+	 */
+
+#ifdef asdsdfa
+	/* First find out the type of the period's columns */
+	// TODO: I'm doing this in DefineRelation now,
+	// which seems like a better place since it knows about inherited columns.
+	period->coltypid = InvalidOid;
+	period->rngtypid = InvalidOid;
+	foreach (columns, cxt->columns)
+	{
+		col = (ColumnDef *) lfirst(columns);
+		if (strcmp(col->colname, period->startcolname) == 0)
+		{
+			coltypid = typenameTypeId(cxt->pstate, col->typeName);
+			break;
+		}
+	}
+	if (coltypid == InvalidOid)
+		ereport(ERROR, (errmsg("column \"%s\" of relation \"%s\" does not exist",
+						period->startcolname, cxt->relation->relname)));
+
+	period->coltypid = coltypid;
+
+	/* Now make sure it matches rangetypename or we can find a matching range */
+	period->rngtypid = choose_rangetype_for_period(period);
+#endif
+
+	cxt->periods = lappend(cxt->periods, period);
+}
+
 /*
  * transformTableConstraint
  *		transform a Constraint node within CREATE TABLE or ALTER TABLE
@@ -1003,6 +1120,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	AttrNumber	parent_attno;
 	Relation	relation;
 	TupleDesc	tupleDesc;
+	Bitmapset  *periodatts;
 	AclResult	aclresult;
 	char	   *comment;
 	ParseCallbackState pcbstate;
@@ -1055,6 +1173,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}
 
 	tupleDesc = RelationGetDescr(relation);
+	periodatts = get_period_attnos(RelationGetRelid(relation));
 
 	/*
 	 * Insert the copied attributes into the cxt for the new table definition.
@@ -1064,10 +1183,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	for (parent_attno = 1; parent_attno <= tupleDesc->natts;
 		 parent_attno++)
 	{
-		Form_pg_attribute attribute = TupleDescAttr(tupleDesc,
-													parent_attno - 1);
+		Form_pg_attribute attribute;
 		ColumnDef  *def;
 
+		/*
+		 * If this column is from a PERIOD, skip it
+		 * (since LIKE never copies PERIODs).
+		 */
+		if (bms_is_member(parent_attno, periodatts))
+			continue;
+
+		attribute = TupleDescAttr(tupleDesc, parent_attno - 1);
+
 		/*
 		 * Ignore dropped columns in the parent.
 		 */
@@ -3008,6 +3135,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 		}
 	}
 
+	/* take care of the period */
+	if (stmt->period)
+		stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
 	/*
 	 * Check that only the base rel is mentioned.  (This should be dead code
 	 * now that add_missing_from is history.)
@@ -3465,6 +3596,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.inhRelations = NIL;
 	cxt.isalter = true;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.nnconstraints = NIL;
 	cxt.fkconstraints = NIL;
@@ -3527,6 +3659,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
+					newcmds = lappend(newcmds, cmd);
+					// Why not call transformTablePeriod here?
+					// Ah because it looks at cxt->columns
+					// and in an ALTER statement the columns might already exist (or not).
+					break;
+				}
+
 			case AT_AlterColumnType:
 				{
 					ColumnDef  *def = castNode(ColumnDef, cmd->def);
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 199eae525d1..d04c75b3980 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -78,6 +78,7 @@ OBJS = \
 	oracle_compat.o \
 	orderedsetaggs.o \
 	partitionfuncs.o \
+	period.o \
 	pg_locale.o \
 	pg_lsn.o \
 	pg_upgrade_support.o \
diff --git a/src/backend/utils/adt/period.c b/src/backend/utils/adt/period.c
new file mode 100644
index 00000000000..0ed4304e16d
--- /dev/null
+++ b/src/backend/utils/adt/period.c
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.c
+ *	  Functions to support periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/period.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "executor/tuptable.h"
+#include "fmgr.h"
+#include "nodes/primnodes.h"
+#include "utils/fmgrprotos.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+
+Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype)
+{
+	Datum startvalue;
+	Datum endvalue;
+	Datum result;
+	bool	startisnull;
+	bool	endisnull;
+	LOCAL_FCINFO(fcinfo, 2);
+	FmgrInfo	flinfo;
+	FuncExpr   *f;
+
+	InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, NULL);
+	f = makeNode(FuncExpr);
+	f->funcresulttype = rangetype;
+	flinfo.fn_expr = (Node *) f;
+	flinfo.fn_extra = NULL;
+
+	/* compute oldvalue */
+	startvalue = slot_getattr(slot, startattno, &startisnull);
+	endvalue = slot_getattr(slot, endattno, &endisnull);
+
+	fcinfo->args[0].value = startvalue;
+	fcinfo->args[0].isnull = startisnull;
+	fcinfo->args[1].value = endvalue;
+	fcinfo->args[1].isnull = endisnull;
+
+	result = range_constructor2(fcinfo);
+	if (fcinfo->isnull)
+		elog(ERROR, "function %u returned NULL", flinfo.fn_oid);
+
+	return result;
+}
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 9a24882f895..54404c54a71 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -30,6 +30,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_range.h"
 #include "catalog/pg_statistic.h"
@@ -994,6 +995,68 @@ get_attoptions(Oid relid, int16 attnum)
 	return result;
 }
 
+/*				---------- PG_PERIOD CACHE ----------				 */
+
+/*
+ * get_periodname - given its OID, look up a period
+ *
+ * If missing_ok is false, throw an error if the period is not found.
+ * If true, just return InvalidOid.
+ */
+char *
+get_periodname(Oid periodid, bool missing_ok)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(PERIODOID,
+						 ObjectIdGetDatum(periodid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+		char	   *result;
+
+		result = pstrdup(NameStr(period_tup->pername));
+		ReleaseSysCache(tp);
+		return result;
+	}
+
+	if (!missing_ok)
+		elog(ERROR, "cache lookup failed for period %d",
+			 periodid);
+	return NULL;
+}
+
+/*
+ * get_period_oid - gets its relation and name, look up a period
+ *
+ * If missing_ok is false, throw an error if the cast is not found.  If
+ * true, just return InvalidOid.
+ */
+Oid
+get_period_oid(Oid relid, const char *periodname, bool missing_ok)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache2(PERIODNAME,
+						 ObjectIdGetDatum(relid),
+						 PointerGetDatum(periodname));
+
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp);
+		Oid result;
+
+		result = period_tup->oid;
+		ReleaseSysCache(tp);
+		return result;
+	}
+
+	if (!missing_ok)
+		elog(ERROR, "cache lookup failed for period %s",
+			 periodname);
+	return InvalidOid;
+}
+
 /*				---------- PG_CAST CACHE ----------					 */
 
 /*
@@ -3537,6 +3600,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
+get_subtype_range(Oid subtypeOid)
+{
+	CatCList *catlist;
+	Oid	result = InvalidOid;
+
+	catlist = SearchSysCacheList1(RANGESUBTYPE, ObjectIdGetDatum(subtypeOid));
+
+	if (catlist->n_members == 1)
+	{
+		HeapTuple	tuple = &catlist->members[0]->tuple;
+		Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tuple);
+		result = rngtup->rngtypid;
+		ReleaseCatCacheList(catlist);
+	}
+	else if (catlist->n_members > 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INDETERMINATE_DATATYPE),
+				 errmsg("ambiguous range for type %s",
+						format_type_be(subtypeOid))));
+
+	return result;
+}
+
 /*				---------- PG_INDEX CACHE ----------				 */
 
 /*
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index c10a7b3d0d2..efd8450baef 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_publication.h"
 #include "catalog/pg_publication_namespace.h"
@@ -422,6 +423,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_partitioned_table_partrelid),
 		32
 	},
+	[PERIODNAME] = {
+		PeriodRelationId,
+		PeriodRelidNameIndexId,
+		KEY(Anum_pg_period_perrelid,
+			Anum_pg_period_pername),
+		32
+	},
+	[PERIODOID] = {
+		PeriodRelationId,
+		PeriodObjectIndexId,
+		KEY(Anum_pg_period_oid),
+		32
+	},
 	[PROCNAMEARGSNSP] = {
 		ProcedureRelationId,
 		ProcedureNameArgsNspIndexId,
@@ -480,6 +494,13 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_range_rngmultitypid),
 		4
 	},
+	[RANGESUBTYPE] = {
+		RangeRelationId,
+		RangeSubTypidTypidIndexId,
+		KEY(Anum_pg_range_rngsubtype,
+			Anum_pg_range_rngtypid),
+		4
+	},
 	[RANGETYPE] = {
 		RangeRelationId,
 		RangeTypidIndexId,
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4e..0a2696c5597 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3518,6 +3518,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 			 strcmp(type, "DATABASE PROPERTIES") == 0 ||
 			 strcmp(type, "DEFAULT") == 0 ||
 			 strcmp(type, "FK CONSTRAINT") == 0 ||
+			 strcmp(type, "PERIOD") == 0 ||
 			 strcmp(type, "INDEX") == 0 ||
 			 strcmp(type, "RULE") == 0 ||
 			 strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd6..b13cf284827 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6600,6 +6600,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reltype;
 	int			i_relowner;
 	int			i_relchecks;
+	int			i_nperiod;
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
@@ -6677,6 +6678,14 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
+	/* In PG16 upwards we have PERIODs. */
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBufferStr(query,
+							 "(SELECT count(*) FROM pg_period WHERE perrelid = c.oid) AS nperiods, ");
+	else
+		appendPQExpBufferStr(query,
+							 "0 AS nperiods, ");
+
 	if (fout->remoteVersion >= 90300)
 		appendPQExpBufferStr(query,
 							 "c.relispopulated, ");
@@ -6814,6 +6823,7 @@ getTables(Archive *fout, int *numTables)
 	i_reltype = PQfnumber(res, "reltype");
 	i_relowner = PQfnumber(res, "relowner");
 	i_relchecks = PQfnumber(res, "relchecks");
+	i_nperiod = PQfnumber(res, "nperiods");
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
@@ -6897,6 +6907,7 @@ getTables(Archive *fout, int *numTables)
 		}
 		tblinfo[i].reltablespace = pg_strdup(PQgetvalue(res, i, i_reltablespace));
 		tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
+		tblinfo[i].nperiod = atoi(PQgetvalue(res, i, i_nperiod));
 		tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
 		tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence));
 		tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
@@ -8561,7 +8572,7 @@ getTransforms(Archive *fout, int *numTransforms)
 /*
  * getTableAttrs -
  *	  for each interesting table, read info about its attributes
- *	  (names, types, default values, CHECK constraints, etc)
+ *	  (names, types, default values, CHECK constraints, PERIODs, etc)
  *
  *	modifies tblinfo
  */
@@ -8614,6 +8625,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	for (int i = 0; i < numTables; i++)
 	{
 		TableInfo  *tbinfo = &tblinfo[i];
+		int			ndumpablechecks;	/* number of CHECK constraints that do
+										   not belong to a period */
 
 		/* Don't bother to collect info for sequences */
 		if (tbinfo->relkind == RELKIND_SEQUENCE)
@@ -8628,7 +8641,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			appendPQExpBufferChar(tbloids, ',');
 		appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
 
-		if (tbinfo->ncheck > 0)
+		ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+		if (ndumpablechecks > 0)
 		{
 			/* Also make a list of the ones with check constraints */
 			if (checkoids->len > 1) /* do we have more than the '{'? */
@@ -9167,15 +9181,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		pg_log_info("finding table check constraints");
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,
-						  "SELECT c.tableoid, c.oid, conrelid, conname, "
-						  "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
-						  "conislocal, convalidated "
-						  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
-						  "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
-						  "WHERE contype = 'c' "
-						  "ORDER BY c.conrelid, c.conname",
-						  checkoids->data);
+		if (fout->remoteVersion >= 160000)
+		{
+			/*
+			 * PERIODs were added in v17 and we don't dump CHECK
+			 * constraints for them.
+			 */
+			appendPQExpBuffer(q,
+							  "SELECT c.tableoid, c.oid, conrelid, conname, "
+							  "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+							  "conislocal, convalidated "
+							  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+							  "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+							  "WHERE contype = 'c' "
+							  "   AND NOT EXISTS (SELECT FROM pg_period "
+							  "                   WHERE (perrelid, perconstraint) = (conrelid, c.oid)) "
+							  "ORDER BY c.conrelid, c.conname",
+							  checkoids->data);
+		}
+		else
+		{
+			appendPQExpBuffer(q,
+							  "SELECT c.tableoid, c.oid, conrelid, conname, "
+							  "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+							  "conislocal, convalidated "
+							  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+							  "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+							  "WHERE contype = 'c' "
+							  "ORDER BY c.conrelid, c.conname",
+							  checkoids->data);
+		}
 
 		res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
 
@@ -9197,6 +9232,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
 			/* Count rows for this table */
 			for (numcons = 1; numcons < numConstrs - j; numcons++)
@@ -9216,12 +9252,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			if (curtblindx >= numTables)
 				pg_fatal("unrecognized table OID %u", conrelid);
 
-			if (numcons != tbinfo->ncheck)
+			ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod;
+			if (numcons != ndumpablechecks)
 			{
 				pg_log_error(ngettext("expected %d check constraint on table \"%s\" but found %d",
 									  "expected %d check constraints on table \"%s\" but found %d",
-									  tbinfo->ncheck),
-							 tbinfo->ncheck, tbinfo->dobj.name, numcons);
+									  ndumpablechecks),
+							 ndumpablechecks, tbinfo->dobj.name, numcons);
 				pg_log_error_hint("The system catalogs might be corrupted.");
 				exit_nicely(1);
 			}
@@ -9280,6 +9317,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+	for (int i = 0; i < numTables; i++)
+	{
+		TableInfo  *tbinfo = &tblinfo[i];
+
+		/*
+		 * Get info about PERIOD definitions
+		 */
+		if (tbinfo->nperiod > 0)
+		{
+			PeriodInfo *periods;
+			int			numPeriods;
+			int			j;
+
+			/* We shouldn't have any periods before v17 */
+			Assert(fout->remoteVersion >= 160000);
+
+			pg_log_info("finding periods for table \"%s.%s\"",
+						tbinfo->dobj.namespace->dobj.name,
+						tbinfo->dobj.name);
+
+			resetPQExpBuffer(q);
+			appendPQExpBuffer(q,
+				"SELECT p.tableoid, p.oid, p.pername, "
+				"       sa.attname AS perstart, ea.attname AS perend, "
+				"       r.typname AS rngtype, "
+				"       c.conname AS conname "
+				"FROM pg_catalog.pg_period AS p "
+				"JOIN pg_catalog.pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart) "
+				"JOIN pg_catalog.pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend) "
+				"JOIN pg_catalog.pg_type AS r ON r.oid = p.perrngtype "
+				"JOIN pg_catalog.pg_constraint AS c ON c.oid = p.perconstraint "
+				"WHERE p.perrelid = '%u'::pg_catalog.oid "
+				"ORDER BY p.pername",
+				tbinfo->dobj.catId.oid);
+
+			res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+			/*
+			 * If we didn't get the number of rows we thought we were going to,
+			 * then those JOINs didn't work.
+			 */
+			numPeriods = PQntuples(res);
+			if (numPeriods != tbinfo->nperiod)
+			{
+				pg_log_info(ngettext("expected %d period on table \"%s\" but found %d",
+									 "expected %d periods on table \"%s\" but found %d",
+									 tbinfo->nperiod),
+							tbinfo->nperiod, tbinfo->dobj.name, numPeriods);
+				pg_log_info("(The system catalogs might be corrupted.)");
+				exit_nicely(1);
+			}
+
+			periods = (PeriodInfo *) pg_malloc(numPeriods * sizeof(PeriodInfo));
+			tbinfo->periods = periods;
+
+			for (j = 0; j < numPeriods; j++)
+			{
+				periods[j].dobj.objType = DO_PERIOD;
+				periods[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, 0));
+				periods[j].dobj.catId.oid = atooid(PQgetvalue(res, j, 1));
+				AssignDumpId(&periods[j].dobj);
+				periods[j].dobj.name = pg_strdup(PQgetvalue(res, j, 2));
+				periods[j].dobj.namespace = tbinfo->dobj.namespace;
+				periods[j].pertable = tbinfo;
+				periods[j].perstart = pg_strdup(PQgetvalue(res, j, 3));
+				periods[j].perend = pg_strdup(PQgetvalue(res, j, 4));
+				periods[j].rngtype = pg_strdup(PQgetvalue(res, j, 5));
+				periods[j].conname = pg_strdup(PQgetvalue(res, j, 6));
+			}
+			PQclear(res);
+		}
+	}
+
 	destroyPQExpBuffer(q);
 	destroyPQExpBuffer(tbloids);
 	destroyPQExpBuffer(checkoids);
@@ -10550,6 +10660,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_FK_CONSTRAINT:
 			dumpConstraint(fout, (const ConstraintInfo *) dobj);
 			break;
+		case DO_PERIOD:
+			break;
 		case DO_PROCLANG:
 			dumpProcLang(fout, (const ProcLangInfo *) dobj);
 			break;
@@ -16088,6 +16200,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
+			 * Add non-inherited PERIOD definitions, if any.
+			 */
+			for (j = 0; j < tbinfo->nperiod; j++)
+			{
+				PeriodInfo *period = &(tbinfo->periods[j]);
+
+				char	   *name = pg_strdup(fmtId(period->dobj.name));
+				char	   *start = pg_strdup(fmtId(period->perstart));
+				char	   *end = pg_strdup(fmtId(period->perend));
+				char	   *rngtype = pg_strdup(fmtId(period->rngtype));
+				char	   *conname = pg_strdup(fmtId(period->conname));
+
+				if (actual_atts == 0)
+					appendPQExpBufferStr(q, " (\n    ");
+				else
+					appendPQExpBufferStr(q, ",\n    ");
+
+				appendPQExpBuffer(q, "PERIOD FOR %s (%s, %s) "
+						"WITH (rangetype = %s, check_constraint_name = %s)",
+								  name, start, end,
+								  rngtype, conname);
+
+				actual_atts++;
+			}
+
 			/*
 			 * Add non-inherited CHECK constraints, if any.
 			 *
@@ -16096,7 +16234,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 * PARTITION that we'll emit later expects the constraint to be
 			 * there.  (No need to fix conislocal: ATTACH PARTITION does that)
 			 */
-			for (j = 0; j < tbinfo->ncheck; j++)
+			for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
 
@@ -16314,7 +16452,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 * For partitions, they were already dumped, and conislocal
 			 * doesn't need fixing.
 			 */
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -16616,7 +16754,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);
 
 	/* Dump comments on inlined table constraints */
-	for (j = 0; j < tbinfo->ncheck; j++)
+	for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++)
 	{
 		ConstraintInfo *constr = &(tbinfo->checkexprs[j]);
 
@@ -18585,6 +18723,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f523c3a590d..f4635b12887 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -59,6 +59,7 @@ typedef enum
 	DO_TRIGGER,
 	DO_CONSTRAINT,
 	DO_FK_CONSTRAINT,			/* see note for ConstraintInfo */
+	DO_PERIOD,
 	DO_PROCLANG,
 	DO_CAST,
 	DO_TABLE_DATA,
@@ -300,12 +301,14 @@ typedef struct _tableInfo
 	bool		rowsec;			/* is row security enabled? */
 	bool		forcerowsec;	/* is row security forced? */
 	bool		hasoids;		/* does it have OIDs? */
+	bool		hasperiods;		/* does it have any periods? */
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
+	int			nperiod;		/* # of PERIOD definitions */
 	Oid			reltype;		/* OID of table's composite type, if any */
 	Oid			reloftype;		/* underlying type for typed table */
 	Oid			foreign_server; /* foreign server oid, if applicable */
@@ -355,6 +358,7 @@ typedef struct _tableInfo
 	bool	   *notnull_inh;	/* true if NOT NULL has no local definition */
 	struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
 	struct _constraintInfo *checkexprs; /* CHECK constraints */
+	struct _periodInfo *periods;	/* PERIOD definitions */
 	bool		needs_override; /* has GENERATED ALWAYS AS IDENTITY */
 	char	   *amname;			/* relation access method */
 
@@ -484,6 +488,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
+typedef struct _periodInfo
+{
+	DumpableObject dobj;
+	TableInfo  *pertable;
+	char	   *perstart;		/* the name of the start column */
+	char	   *perend;			/* the name of the end column */
+	char	   *rngtype;		/* the name of the range type */
+	char	   *conname;		/* the name of the CHECK constraint */
+} PeriodInfo;
+
 typedef struct _procLangInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index f358dd22b9d..b25b3862ab2 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -85,6 +85,7 @@ enum dbObjectTypePriorities
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
 	PRIO_INDEX_ATTACH,
+	PRIO_PERIOD,
 	PRIO_STATSEXT,
 	PRIO_RULE,
 	PRIO_TRIGGER,
@@ -120,6 +121,7 @@ static const int dbObjectTypePriority[] =
 	PRIO_ATTRDEF,				/* DO_ATTRDEF */
 	PRIO_INDEX,					/* DO_INDEX */
 	PRIO_INDEX_ATTACH,			/* DO_INDEX_ATTACH */
+	PRIO_PERIOD,				/* DO_PERIOD */
 	PRIO_STATSEXT,				/* DO_STATSEXT */
 	PRIO_RULE,					/* DO_RULE */
 	PRIO_TRIGGER,				/* DO_TRIGGER */
@@ -1370,6 +1372,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "FK CONSTRAINT %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PERIOD:
+			snprintf(buf, bufsize,
+					 "PERIOD %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PROCLANG:
 			snprintf(buf, bufsize,
 					 "PROCEDURAL LANGUAGE %s  (ID %d OID %u)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 3f8bff59548..61c69f11ed9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1943,6 +1943,8 @@ describeOneTableDetails(const char *schemaname,
 
 	appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_attribute a");
 	appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attnum > 0 AND NOT a.attisdropped", oid);
+	if (pset.sversion >= 170000)
+		appendPQExpBuffer(&buf, "\nAND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_period p WHERE p.perrelid = a.attrelid AND p.perrange = a.attnum)");
 	appendPQExpBufferStr(&buf, "\nORDER BY a.attnum;");
 
 	res = PSQLexec(buf.data);
@@ -2365,6 +2367,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */
+		if (pset.sversion >= 170000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT quote_ident(p.pername), quote_ident(s.attname) AS startatt, quote_ident(e.attname) AS endatt\n"
+							  "FROM pg_period AS p\n"
+							  "JOIN pg_attribute AS s ON (s.attrelid, s.attnum) = (p.perrelid, p.perstart)\n"
+							  "JOIN pg_attribute AS e ON (e.attrelid, e.attnum) = (p.perrelid, p.perend)\n"
+							  "WHERE p.perrelid = '%s'\n"
+							  "ORDER BY 1;",
+							  oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Periods:"));
+				for (i = 0; i < tuples; i++)
+				{
+					/* untranslated constraint name and def */
+					printfPQExpBuffer(&buf, "    %s (%s, %s)",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1),
+									  PQgetvalue(result, i, 2));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print indexes */
 		if (tableinfo.hasindex)
 		{
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index fd588659014..e84d90c0e5e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -93,6 +93,7 @@ typedef enum ObjectClass
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
+	OCLASS_PERIOD,				/* pg_period */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
 	OCLASS_LANGUAGE,			/* pg_language */
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 1d7f8380d90..3592a444bd1 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -121,6 +121,10 @@ extern List *AddRelationNotNullConstraints(Relation rel,
 extern void RelationClearMissing(Relation rel);
 extern void SetAttrMissing(Oid relid, char *attname, char *value);
 
+extern Oid StorePeriod(Relation rel, const char *period,
+					   AttrNumber startnum, AttrNumber endnum,
+					   AttrNumber rangenum, Oid conoid);
+
 extern Node *cookDefault(ParseState *pstate,
 						 Node *raw_default,
 						 Oid atttypid,
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 2bcbe327cfe..5596c523b7e 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -61,6 +61,7 @@ catalog_headers = [
   'pg_collation.h',
   'pg_parameter_acl.h',
   'pg_partitioned_table.h',
+  'pg_period.h',
   'pg_range.h',
   'pg_transform.h',
   'pg_sequence.h',
diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h
index e4f3f708071..b4ccf2fa69e 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -48,7 +48,6 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO
 	/* variable-length fields start here, but we allow direct access to indkey */
 	int2vector	indkey BKI_FORCE_NOT_NULL;	/* column numbers of indexed cols,
 											 * or 0 */
-
 #ifdef CATALOG_VARLEN
 	oidvector	indcollation BKI_LOOKUP_OPT(pg_collation) BKI_FORCE_NOT_NULL;	/* collation identifiers */
 	oidvector	indclass BKI_LOOKUP(pg_opclass) BKI_FORCE_NOT_NULL; /* opclass identifiers */
diff --git a/src/include/catalog/pg_period.h b/src/include/catalog/pg_period.h
new file mode 100644
index 00000000000..d2cffa07bcd
--- /dev/null
+++ b/src/include/catalog/pg_period.h
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_period.h
+ *	  definition of the "period" system catalog (pg_period)
+ *
+ *
+ * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_period.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PERIOD_H
+#define PG_PERIOD_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_period_d.h"
+
+/* ----------------
+ *		pg_period definition.  cpp turns this into
+ *		typedef struct FormData_pg_period
+ * ----------------
+ */
+CATALOG(pg_period,8000,PeriodRelationId)
+{
+	Oid			oid;			/* OID of the period */
+	NameData	pername;		/* name of period */
+	Oid			perrelid;		/* OID of relation containing this period */
+	int16		perstart;		/* column for start value */
+	int16		perend;			/* column for end value */
+	int16		perrange;		/* column for range value */
+	Oid			perconstraint;	/* OID of (start < end) constraint */
+} FormData_pg_period;
+
+/* ----------------
+ *		Form_pg_period corresponds to a pointer to a tuple with
+ *		the format of pg_period relation.
+ * ----------------
+ */
+typedef FormData_pg_period *Form_pg_period;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_period_oid_index, 8001, PeriodObjectIndexId, pg_period, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_period_perrelid_pername_index, 8002, PeriodRelidNameIndexId, pg_period, btree(perrelid oid_ops, pername name_ops));
+
+extern void RemovePeriodById(Oid periodId);
+
+extern Oid get_relation_period_oid(Oid relid, const char *pername, bool missing_ok);
+extern Bitmapset *get_period_attnos(Oid relid);
+
+#endif							/* PG_PERIOD_H */
diff --git a/src/include/catalog/pg_range.h b/src/include/catalog/pg_range.h
index a1f527130bf..d97a95bd2fb 100644
--- a/src/include/catalog/pg_range.h
+++ b/src/include/catalog/pg_range.h
@@ -59,6 +59,7 @@ typedef FormData_pg_range *Form_pg_range;
 
 DECLARE_UNIQUE_INDEX_PKEY(pg_range_rngtypid_index, 3542, RangeTypidIndexId, pg_range, btree(rngtypid oid_ops));
 DECLARE_UNIQUE_INDEX(pg_range_rngmultitypid_index, 2228, RangeMultirangeTypidIndexId, pg_range, btree(rngmultitypid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_range_rngsubtype_rngtypid_index, 8003, RangeSubTypidTypidIndexId, pg_range, btree(rngsubtype oid_ops, rngtypid oid_ops));
 
 /*
  * prototypes for functions in pg_range.c
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 96769472bbb..89e538c7504 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -40,7 +40,8 @@ extern LOCKMODE AlterTableGetLockLevel(List *cmds);
 
 extern void ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode);
 
-extern void AlterTableInternal(Oid relid, List *cmds, bool recurse);
+extern void AlterTableInternal(Oid relid, List *cmds, bool recurse,
+							   struct AlterTableUtilityContext *context);
 
 extern Oid	AlterTableMoveAll(AlterTableMoveAllStmt *stmt);
 
@@ -108,5 +109,6 @@ extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 extern void FindFKPeriodOpersAndProcs(Oid opclass,
 									  Oid *periodoperoid,
 									  Oid *periodprocoid);
+extern Oid choose_rangetype_for_period(PeriodDef *period);
 
 #endif							/* TABLECMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index bdada6b4252..a6262559be2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2141,6 +2141,7 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_PARAMETER_ACL,
+	OBJECT_PERIOD,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
@@ -2229,6 +2230,8 @@ typedef enum AlterTableType
 	AT_ValidateConstraint,		/* validate constraint */
 	AT_AddIndexConstraint,		/* add constraint using existing index */
 	AT_DropConstraint,			/* drop constraint */
+	AT_AddPeriod,				/* ADD PERIOD */
+	AT_DropPeriod,				/* DROP PERIOD */
 	AT_ReAddComment,			/* internal to commands/tablecmds.c */
 	AT_AlterColumnType,			/* alter column type */
 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
@@ -2495,11 +2498,11 @@ typedef struct VariableShowStmt
 /* ----------------------
  *		Create Table Statement
  *
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints and nnconstraints are NIL.  After
- * parse analysis, tableElts contains just ColumnDefs, nnconstraints contains
- * Constraint nodes of CONSTR_NOTNULL type from various sources, and
- * constraints contains just CONSTR_CHECK Constraint nodes.
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; periods, constraints, and nnconstraints are NIL.  After
+ * parse analysis, tableElts contains just ColumnDefs, periods contains just PeriodDefs,
+ * nnconstraints contains Constraint nodes of CONSTR_NOTNULL type from various sources,
+ * and constraints contains just CONSTR_CHECK Constraint nodes.
  * ----------------------
  */
 
@@ -2508,6 +2511,7 @@ typedef struct CreateStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to create */
 	List	   *tableElts;		/* column definitions (list of ColumnDef) */
+	List	   *periods;		/* periods (list of PeriodDef nodes) */
 	List	   *inhRelations;	/* relations to inherit from (list of
 								 * RangeVar) */
 	PartitionBoundSpec *partbound;	/* FOR VALUES clause */
@@ -2522,6 +2526,30 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
+ * Definitions for periods in CreateStmt
+ * ----------
+ */
+
+typedef struct PeriodDef
+{
+	NodeTag		type;
+	Oid			oid;			/* period oid, once it's transformed */
+	char	   *periodname;		/* period name */
+	char	   *startcolname;	/* name of start column */
+	char	   *endcolname;		/* name of end column */
+	AttrNumber	startattnum;	/* attnum of the start column */
+	AttrNumber	endattnum;		/* attnum of the end column */
+	AttrNumber	rngattnum;		/* attnum of the GENERATED range column */
+	List	   *options;		/* options from WITH clause */
+	char	   *constraintname;	/* name of the CHECK constraint */
+	char	   *rangetypename;	/* name of the range type */
+	Oid			coltypid;		/* the start/end col type */
+	Oid			rngtypid;		/* the range type to use */
+	int			location;		/* token location, or -1 if unknown */
+} PeriodDef;
+
 /* ----------
  * Definitions for constraints in CreateStmt
  *
@@ -3233,6 +3261,7 @@ typedef struct IndexStmt
 	List	   *indexParams;	/* columns to index: a list of IndexElem */
 	List	   *indexIncludingParams;	/* additional columns to index: a list
 										 * of IndexElem */
+	PeriodDef  *period;			/* The period included in the index */
 	List	   *options;		/* WITH clause options: a list of DefElem */
 	Node	   *whereClause;	/* qualification (partial-index predicate) */
 	List	   *excludeOpNames; /* exclusion operator names, or NIL if none */
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 14065894779..d84d02b0280 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -40,5 +40,6 @@ extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel,
 										  Relation source_idx,
 										  const struct AttrMap *attmap,
 										  Oid *constraintOid);
+extern void transformPeriodOptions(PeriodDef *period);
 
 #endif							/* PARSE_UTILCMD_H */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 9487873ad48..c3ad230ea65 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -95,6 +95,8 @@ extern Oid	get_atttype(Oid relid, AttrNumber attnum);
 extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
 								  Oid *typid, int32 *typmod, Oid *collid);
 extern Datum get_attoptions(Oid relid, int16 attnum);
+extern char *get_periodname(Oid periodid, bool missing_ok);
+extern Oid	get_period_oid(Oid relid, const char *periodname, bool missing_ok);
 extern Oid	get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
 extern char *get_collation_name(Oid colloid);
 extern bool get_collation_isdeterministic(Oid colloid);
@@ -197,6 +199,7 @@ extern Oid	get_range_subtype(Oid rangeOid);
 extern Oid	get_range_collation(Oid rangeOid);
 extern Oid	get_range_multirange(Oid rangeOid);
 extern Oid	get_multirange_range(Oid multirangeOid);
+extern Oid	get_subtype_range(Oid subtypeOid);
 extern Oid	get_index_column_opclass(Oid index_oid, int attno);
 extern bool get_index_isreplident(Oid index_oid);
 extern bool get_index_isvalid(Oid index_oid);
diff --git a/src/include/utils/period.h b/src/include/utils/period.h
new file mode 100644
index 00000000000..0a8af3edb01
--- /dev/null
+++ b/src/include/utils/period.h
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * period.h
+ *	  support for Postgres periods.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/period.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PERIOD_H
+#define PERIOD_H
+
+extern Datum period_to_range(TupleTableSlot *slot, int startattno, int endattno, Oid rangetype);
+
+#endif							/* PERIOD_H */
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 3fb3314b6ca..c942e867386 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -75,6 +75,8 @@ enum SysCacheIdentifier
 	PARAMETERACLNAME,
 	PARAMETERACLOID,
 	PARTRELID,
+	PERIODNAME,
+	PERIODOID,
 	PROCNAMEARGSNSP,
 	PROCOID,
 	PUBLICATIONNAME,
@@ -84,6 +86,7 @@ enum SysCacheIdentifier
 	PUBLICATIONREL,
 	PUBLICATIONRELMAP,
 	RANGEMULTIRANGE,
+	RANGESUBTYPE,
 	RANGETYPE,
 	RELNAMENSP,
 	RELOID,
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 48563b2cf01..a954e703384 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -183,6 +183,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
 			case AT_DropConstraint:
 				strtype = "DROP CONSTRAINT";
 				break;
+			case AT_AddPeriod:
+				strtype = "ADD PERIOD";
+				break;
+			case AT_DropPeriod:
+				strtype = "DROP PERIOD";
+				break;
 			case AT_ReAddComment:
 				strtype = "(re) ADD COMMENT";
 				break;
diff --git a/src/test/regress/expected/periods.out b/src/test/regress/expected/periods.out
new file mode 100644
index 00000000000..6481c9b3056
--- /dev/null
+++ b/src/test/regress/expected/periods.out
@@ -0,0 +1,177 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+ERROR:  PERIOD FOR SYSTEM_TIME is not supported
+LINE 2: create table pt (id integer, ds date, de date, period for sy...
+                                                       ^
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+ERROR:  column "bogus" of relation "pt" does not exist
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+ERROR:  column "bogus" of relation "pt" does not exist
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+ERROR:  start and end columns of period must be of same type
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+ERROR:  start and end columns of period must have same collation
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+ERROR:  no range type for xml found for period p
+HINT:  You can define a custom range type with CREATE TYPE
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+ERROR:  period name "ctid" conflicts with a system column name
+create table pt (id integer, ds date, de date, period for id (ds, de));
+ERROR:  period name "id" conflicts with a column name
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+ERROR:  period name "p" specified more than once
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+ id |     ds     |     de     
+----+------------+------------
+  1 | 01-01-2000 | 01-01-2001
+(1 row)
+
+/* You can get it if you want */
+select *, p from pt;
+ id |     ds     |     de     |            p            
+----+------------+------------+-------------------------
+  1 | 01-01-2000 | 01-01-2001 | [01-01-2000,01-01-2001)
+(1 row)
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+ERROR:  PERIOD FOR SYSTEM_TIME is not supported
+alter table pt add period for p (ds, de);
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+/* Can't drop its columns */
+alter table pt drop column ds;
+ERROR:  cannot drop column ds of table pt because other objects depend on it
+DETAIL:  period p on table pt depends on column ds of table pt
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+alter table pt drop column de;
+ERROR:  cannot drop column de of table pt because other objects depend on it
+DETAIL:  period p on table pt depends on column de of table pt
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+ERROR:  cannot alter type of a column used by a period
+DETAIL:  period p on table pt depends on column "ds"
+alter table pt alter column ds type timestamp;
+ERROR:  cannot alter type of a column used by a period
+DETAIL:  period p on table pt depends on column "ds"
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+ERROR:  column name "p" conflicts with a period name
+alter table pt rename column id to p;
+ERROR:  column name "p" conflicts with a period name
+alter table pt add period for tableoid (ds, de);
+ERROR:  period name "tableoid" conflicts with a system column name
+alter table pt add period for "........pg.dropped.4........" (ds, de);
+ERROR:  period name "........pg.dropped.4........" conflicts with a column name
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+ERROR:  ambiguous range for type date
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+ERROR:  cannot drop type mydaterange because other objects depend on it
+DETAIL:  period p on table pt2 depends on type mydaterange
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+drop type mydaterange cascade;
+NOTICE:  drop cascades to period p on table pt2
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+ERROR:  Range type notarange not found
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+ERROR:  Range type tstzrange does not match column type date
+drop table pt2;
+/* CREATE TABLE (LIKE ...) */
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+
+drop table pt2;
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p2 (ds, de)
+Check constraints:
+    "pt2_p2_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+ERROR:  period name "id" conflicts with a column name
+/* CREATE TALBE INHERITS */
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+ERROR:  Inheriting from a table with a PERIOD is not supported
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+ERROR:  Inheriting is not supported when a table has a PERIOD
+drop table not_p;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index cc2f6b46e0c..d6b8b96d88e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -43,7 +43,7 @@ test: copy copyselect copydml insert insert_conflict
 # Note: many of the tests in later groups depend on create_index
 # ----------
 test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist periods
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/periods.sql b/src/test/regress/sql/periods.sql
new file mode 100644
index 00000000000..f0be8902179
--- /dev/null
+++ b/src/test/regress/sql/periods.sql
@@ -0,0 +1,119 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+create table pt (id integer, ds date, de date, period for id (ds, de));
+
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+
+/* You can get it if you want */
+select *, p from pt;
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+alter table pt add period for p (ds, de);
+
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+
+/* Can't drop its columns */
+alter table pt drop column ds;
+alter table pt drop column de;
+
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+alter table pt alter column ds type timestamp;
+
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+alter table pt rename column id to p;
+alter table pt add period for tableoid (ds, de);
+alter table pt add period for "........pg.dropped.4........" (ds, de);
+
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+drop type mydaterange cascade;
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+drop table pt2;
+
+/* CREATE TABLE (LIKE ...) */
+
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+drop table pt2;
+
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+
+/* CREATE TALBE INHERITS */
+
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+
+drop table not_p;
-- 
2.42.0

---

From: Peter Smith <[email-masked]>
Date: Mon, 22 Jan 2024 16:59:35 +1100

2024-01 Commitfest.

Hi, This patch has a CF status of "Needs Review" [1], but it seems
there were CFbot test failures last time it was run [2]. Please have a
look and post an updated version if necessary.

======
[1] https://commitfest.postgresql.org/46/4308/
[2] https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/46/4308

Kind Regards,
Peter Smith.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 24 Jan 2024 17:32:58 +0100

> Here are new patches consolidating feedback from several emails.

I have committed 0001 and 0002 (the primary key support).

The only significant tweak I did was the error messages in 
GetOperatorFromWellKnownStrategy(), to make the messages translatable 
better and share wording with other messages.  These messages are 
difficult to reach, so we'll probably have to wait for someone to 
actually encounter them to see if they are useful.

I would like to work on 0003 and 0004 (the foreign key support) during 
February/March.  The patches beyond that are probably too optimistic for 
PG17.  I recommend you focus getting 0003/0004 in good shape soon.

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 24 Jan 2024 14:06:56 -0800

> I have committed 0001 and 0002 (the primary key support).

Thanks Peter! I noticed the comment on gist_stratnum_btree was out-of-date, so here is a tiny patch 
correcting it.

Also the remaining patches with some updates:

I fixed the dependency issues with PERIODs and their (hidden) GENERATED range columns. This has been 
causing test failures and bugging me since I reordered the patches at PgCon, so I'm glad to finally 
clean it up. The PERIOD should have an INTERNAL dependency on the range column, but then when you 
dropped the table the dependency code thought the whole table was part of the INTERNAL dependency, 
so the drop would fail. The PERIOD patch here fixes the dependency logic. (I guess this is the first 
time a column has been an internal dependency of something.)

I also fixed an error message when you try to change the type of a start/end column used by a 
PERIOD. Previously the error message would complain about the GENERATED column, not the PERIOD, 
which seems confusing. In fact it was non-deterministic, depending on which pg_depend record the 
index returned first.

On 12/6/23 05:22, jian he wrote:
 > tring to the following TODO:
 > // TODO: Need to save context->mtstate->mt_transition_capture? (See
 > comment on ExecInsert)
 >
 > but failed.
 > I also attached the trial, and also added the related test.
 >
 > You can also use the test to check portion update with insert trigger
 > with "referencing old table as old_table new table as new_table"
 > situation.

Thank you for the very helpful test case here. I fixed the issue of not passing along the transition 
table. But there is still more work to do here I think:

- The AFTER INSERT FOR EACH ROW triggers have *both* leftover rows in the NEW table. Now the docs do 
say that for AFTER triggers, a named transition table can see all the changes from the *statement* 
(although that seems pretty weird to me), but the inserts are two *separate* statements. I think the 
SQL:2011 standard is fairly clear about that. So each time the trigger fires we should still get 
just one row in the transition table.

- The AFTER INSERT FOR EACH STATEMENT triggers never fire. That happens outside ExecInsert (in 
ExecModifyTable). In fact there is a bunch of stuff in ExecModifyTable that maybe we need to do when 
we insert leftovers. Do we even need a separate exec node, perhaps wrapping ExecModifyTable? I'm not 
sure that would give us the correct trigger ordering for the triggers on the implicit insert 
statement(s) vs the explicit update/delete statement, so maybe it does all need to be part of the 
single node. But still I think we need to be more careful about memory, especially the per-tuple 
context.

I'll keep working on that, but at least in this round of patches the transition tables aren't 
missing completely.

My plan is still to replace the 'p' amoppurpose operators with just support functions. I want to do 
that next, although as Peter requested I'll also start focusing more narrowly on the foreign key 
patches.

Rebased to 46a0cd4cef.

Yours,

From 29832a724bf0384842bfe68d7c5f17ffab40659c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 24 Jan 2024 12:56:32 -0800
Subject: [PATCH v24 1/7] Fix comment on gist_stratnum_btree

We give results for <, <=, =, >=, and >, not just =. Because why not?
---
 contrib/btree_gist/btree_gist.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/contrib/btree_gist/btree_gist.c b/contrib/btree_gist/btree_gist.c
index c4fc094c652..5fd4cce27d0 100644
--- a/contrib/btree_gist/btree_gist.c
+++ b/contrib/btree_gist/btree_gist.c
@@ -55,7 +55,7 @@ gbt_decompress(PG_FUNCTION_ARGS)
 }
 
 /*
- * Returns the btree number for equals, otherwise invalid.
+ * Returns the btree number for supported operators, otherwise invalid.
  */
 Datum
 gist_stratnum_btree(PG_FUNCTION_ARGS)
-- 
2.42.0

From 93aad6c715c10c8fd3fe44a73dc8df8783b8351c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 29 Dec 2023 14:36:27 -0800
Subject: [PATCH v24 2/7] Add GiST referencedagg support func

diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 8a19f156d83..5a195b96551 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -300,6 +300,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
    used by the operator class.  This lets the core code look up operators for
    temporal constraint indexes.
+   The optional thirteenth method <function>referencedagg</function> is used by
+   temporal foreign keys to combined referenced rows with the same
+   non-<literal>WITHOUT OVERLAPS</literal> into one <literal>WITHOUT OVERLAPS</literal>
+   span to compare against referencing rows.
  </para>
 
  <variablelist>
@@ -1244,6 +1248,114 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15e0..93df136eba3 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
         used by the operator class (optional)</entry>

 	va_start(ap, maxargs);
 
 /*
 
 # gin

From 7801193232431d0ed7d7d4a1d71b4e2ed949950d Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v24 3/7] Add temporal FOREIGN KEYs

 src/backend/commands/indexcmds.c              |  51 +-
 20 files changed, 2349 insertions(+), 383 deletions(-)

index 079af9126ad..6905b75bb5c 100644

 
@@ -1264,6 +1295,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           (There must be a row in the referenced table matching the default
           values, if they are not null, or the operation will fail.)
          </para>
+
+         <para>
+          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>
       </variablelist>
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 7a87626f5f0..47a5c19ce1b 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2175,14 +2175,25 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 		else if (iswithoutoverlaps)
 		{
 			StrategyNumber strat;
+			char	   *opname;
 			Oid			opid;
 
 			if (attn == nkeycols - 1)
+			{
 				strat = RTOverlapStrategyNumber;
+				opname = "overlaps";
+			}
 			else
+			{
 				strat = RTEqualStrategyNumber;
-			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
-											 &opid, &strat);
+				opname = "equals";
+			}
+			GetOperatorFromWellKnownStrategy(opclassOids[attn],
+											 InvalidOid,
+											 opname,
+											 "WITHOUT OVERLAPS constraint",
+											 &opid,
+											 &strat);
 			indexInfo->ii_ExclusionOps[attn] = opid;
 			indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
 			indexInfo->ii_ExclusionStrats[attn] = strat;
@@ -2421,7 +2432,9 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * GetOperatorFromWellKnownStrategy
  *
  * opclass - the opclass to use
- * atttype - the type to ask about
+ * rhstype - the type for the right-hand side
+ * opname - used to build error messages
+ * context - used to build error messages
  * opid - holds the operator we found
  * strat - holds the input and output strategy number
  *
@@ -2434,14 +2447,20 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * InvalidStrategy.
  */
 void
-GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
-								 Oid *opid, StrategyNumber *strat)
+GetOperatorFromWellKnownStrategy(Oid opclass,
+								 Oid rhstype,
+								 const char *opname,
+								 const char *context,
+								 Oid *opid,
+								 StrategyNumber *strat)
 {
 	Oid			opfamily;
 	Oid			opcintype;
 	StrategyNumber instrat = *strat;
 
-	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+	Assert(instrat == RTEqualStrategyNumber ||
+		   instrat == RTOverlapStrategyNumber ||
+		   instrat == RTContainedByStrategyNumber);
 
 	*opid = InvalidOid;
 
@@ -2464,16 +2483,20 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 			ereport(ERROR,
 					errcode(ERRCODE_UNDEFINED_OBJECT),
-					instrat == RTEqualStrategyNumber ?
-					errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-					errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+					errmsg("could not identify a %s operator for type %s for %s", opname, format_type_be(opcintype), context),
 					errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
-							  instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
-
+						 instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
 			ReleaseSysCache(tuple);
 		}

 	if (!OidIsValid(*opid))
@@ -2486,9 +2509,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 		ereport(ERROR,
 				errcode(ERRCODE_UNDEFINED_OBJECT),
-				instrat == RTEqualStrategyNumber ?
-				errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-				errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+				errmsg("could not identify an %s operator for type %s for %s", opname, format_type_be(opcintype), context),
 				errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
 						  NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
 	}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eceacd4ebc3..b8d3a00e4ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@

 
 
 				/*
@@ -9774,6 +9791,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9868,6 +9892,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9886,16 +9921,38 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9904,34 +9961,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	checkFkeyPermissions(pkrel, pkattnum, numpks);
 
-	/*
@@ -9954,189 +9983,68 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 

+	/*
@@ -10153,7 +10061,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10169,7 +10078,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10254,7 +10164,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10340,7 +10251,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10416,7 +10327,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10474,7 +10386,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10522,6 +10435,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10639,7 +10553,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10670,7 +10584,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10906,7 +10821,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10999,6 +10915,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -11114,6 +11031,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
@@ -11145,7 +11063,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
 		/* Set up partition dependencies for the new constraint */
@@ -11179,13 +11097,300 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								is_temporal);
 		table_close(pkrel, NoLock);
 	}

+static void
@@ -11683,7 +11888,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11930,7 +12139,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11996,7 +12204,9 @@ transformColumnNameList(Oid relId, List *colList,
 static int
 transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 						   List **attnamelist,
+						   Node **pk_period,
 						   int16 *attnums, Oid *atttypids,
+						   int16 *periodattnums, Oid *periodatttypids,
 						   Oid *opclasses)
 {
 	List	   *indexoidlist;
@@ -12061,36 +12271,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -12117,6 +12346,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -12138,12 +12371,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -12181,6 +12418,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12290,7 +12540,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12319,8 +12570,10 @@ validateForeignKeyConstraint(char *conname,
 	/*
 	 * See if we can do it with a single LEFT JOIN query.  A false result
 	 * indicates we must proceed with the fire-the-trigger method.
+	 * We can't do a LEFT JOIN for temporal FKs yet,
+	 * but we can once we support temporal left joins.
 	 */
-	if (RI_Initial_Check(&trig, rel, pkrel))
+	if (!temporal && RI_Initial_Check(&trig, rel, pkrel))
 		return;
 
 	/*
@@ -12389,6 +12642,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12408,12 +12662,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12471,37 +12731,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12531,37 +12822,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

 

 
+optionalPeriodName:

 /*
 
 		/* ----------

 

 			quoteOneName(attname,
 
+/* ----------
+ * TRI_FKey_check_ins -
+ *
 
 /*

 

 
 
+	/*
 
 	/*

 /*
- * ri_KeysEqual -
+ * ri_KeysStable -
  *
 
 		if (rel_is_pk)
 		{
-			/*

 	return true;
 }
 
+/*
 
 /*
  * ri_AttributesEqual -

 	return RI_TRIGGER_NONE;
 }
+
+/*

 

 

 
 	return nKeys;
 
+/*
index 01e6bc21cd1..d563e0bd861 100644
 
 	/*
 
 	/*
 
 	/*
@@ -183,7 +183,7 @@ MAKE_SYSCACHE(CONSTROID, pg_constraint_oid_index, 16);

 
index e4115cd0840..9d942cc4191 100644
 
index 0c53d67d3ee..baeea439ee5 100644
@@ -50,8 +50,11 @@ extern bool CheckIndexCompatible(Oid oldId,
-											 Oid *opid, StrategyNumber *strat);

 #endif							/* TABLECMDS_H */

From d408bd0561bec09b3d3e227aeb810f5643c31cd5 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v24 4/7] Add {multi,}range_without_portion proc & operator

 
+/*
index 2d94a6b8774..5a3a0a1e18c 100644
@@ -39,6 +39,7 @@
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/optimizer.h"
@@ -1213,6 +1214,129 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 	return false;
 }

 Datum
 
 ]
index 9d942cc4191..7c11b767758 100644
@@ -10582,6 +10582,9 @@
@@ -10869,6 +10872,9 @@

+-- without_portion
index a7cc220bf0d..a2989bb0918 100644

+-- without_portion
index a5ecdf5372f..1896c7c7539 100644

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

 doc/src/sgml/ref/delete.sgml                  |  69 ++
 src/backend/commands/indexcmds.c              |   4 +-
 src/backend/executor/nodeModifyTable.c        | 274 +++++++-
 src/backend/parser/analyze.c                  | 209 +++++-
 src/backend/utils/adt/ri_triggers.c           |  74 ++
 src/test/regress/expected/for_portion_of.out  | 652 ++++++++++++++++++
 .../regress/expected/without_overlaps.out     | 144 +++-
 src/test/regress/sql/for_portion_of.sql       | 502 ++++++++++++++
 51 files changed, 2646 insertions(+), 72 deletions(-)

index b5a38aeb214..d749e149e5e 100644
 
index f410c3db4e6..0a91ddccdd4 100644

   <table>
@@ -833,8 +835,9 @@

+  <para>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>

 
 
 	/* Now check for inconsistent groups of operators/functions */
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 47a5c19ce1b..f9a0ba1e897 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2460,7 +2460,9 @@ GetOperatorFromWellKnownStrategy(Oid opclass,
 
 	Assert(instrat == RTEqualStrategyNumber ||
 		   instrat == RTOverlapStrategyNumber ||
-		   instrat == RTContainedByStrategyNumber);
+		   instrat == RTContainedByStrategyNumber ||
+		   instrat == RTWithoutPortionStrategyNumber ||
+		   instrat == RTIntersectStrategyNumber);
 
 	*opid = InvalidOid;
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b8d3a00e4ab..2d371fed40d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12613,6 +12613,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

index c344ff09442..27d50cf52fd 100644

 

 

+/* ----------
+ * ForPortionOfState()
+ *
 
index 9fc5abff8f0..f10e4c469c2 100644

@@ -141,6 +143,10 @@ static bool ExecOnConflictUpdate(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,
@@ -160,6 +166,7 @@ static bool ExecMergeMatched(ModifyTableContext *context,
 static void ExecMergeNotMatched(ModifyTableContext *context,
 								ResultRelInfo *resultRelInfo,
 								bool canSetTag);
+static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate);

 /*
@@ -1205,6 +1212,143 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+	CmdType	oldOperation;
+	TransitionCaptureState *oldTcs;
+		/* Save some mtstate things so we can restore them below. */
+		// TODO: Do we need a more systematic way of doing this,
+		// e.g. a new mtstate or even a separate ForPortionOfLeftovers node?
+		oldOperation = mtstate->operation;
+		mtstate->operation = CMD_INSERT;
+		oldTcs = mtstate->mt_transition_capture;
+			ExecSetupTransitionCaptureState(mtstate, estate);
+		mtstate->operation = oldOperation;
+		mtstate->mt_transition_capture = oldTcs;
@@ -1357,7 +1501,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1390,6 +1535,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1770,7 +1920,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2139,6 +2293,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4303,6 +4462,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*

index 2e2458b1284..94257d0c5ab 100644

 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index dbdf6bf8964..268514f8586 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,7 +24,9 @@
 
 #include "postgres.h"

@@ -60,10 +64,16 @@

+static Node *

 

+/*
 
 /*

 

@@ -2523,6 +2707,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 					 errhint("SET target columns cannot be qualified with the relation name.") : 0,
 
+		/*

 
 	QUOTE

 					n->relation = $4;
-					n->usingClause = $5;
-					n->whereClause = $6;
-					n->returningList = $7;
+					n->forPortionOf = $5;
+					n->usingClause = $6;
+					n->whereClause = $7;
+					n->returningList = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}

 					n->relation = $3;
-					n->targetList = $5;
-					n->fromClause = $6;
-					n->whereClause = $7;
-					n->returningList = $8;
+					n->forPortionOf = $4;
+					n->targetList = $6;
+					n->fromClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
 
+for_portion_of_clause:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 /*

 

 

 	/*
 
+/*
+ * get_typname
+ *

 #endif							/* STRATNUM_H */
 
 # gist point_ops
 
 # GiST multirange_ops
 
 # btree multirange_ops
index 570100fad41..c8a6003c738 100644
@@ -99,6 +99,7 @@ MAKE_SYSCACHE(AMOPOPID, pg_amop_opr_fam_index, 64);

 
 /*
index 444a5f0fd57..711c8eec58b 100644
@@ -42,12 +42,14 @@
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -428,6 +430,27 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -548,6 +571,9 @@ typedef struct ResultRelInfo

 
 
+/*
+ * ForPortionOfClause

index 534692bee18..2cd8c38962b 100644
@@ -2364,6 +2364,7 @@ typedef struct ModifyTablePath
 
+/*----------
 
index 00000000000..5bc8f39e060
@@ -0,0 +1,652 @@
+DROP FUNCTION for_portion_of_trigger CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to trigger trg_for_portion_of_before on table for_portion_of_test
+drop cascades to trigger trg_for_portion_of_after on table for_portion_of_test
+-- Triggers with a custom transition table name:
+INSERT INTO for_portion_of_test VALUES ('[1,1]', '[2018-01-01,2020-01-01)', 'one');
+CREATE FUNCTION dump_trigger()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+  RAISE NOTICE 'TG_OP = %', TG_OP;
+  IF TG_OP = 'INSERT' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, NEW table = %',
+      TG_NAME, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'UPDATE' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, OLD table = %, NEW table = %',
+      TG_NAME, TG_LEVEL,
+      (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table),
+      (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'DELETE' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, OLD table = %',
+      TG_NAME, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
+  RETURN NULL;
+$$;
+CREATE TRIGGER for_portion_of_test_insert_trig
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_insert_trig_stmt
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_update_trig
+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();
+CREATE TRIGGER for_portion_of_test_update_trig_stmt
+AFTER UPDATE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig_stmt
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+NOTICE:  TG_OP = INSERT
+NOTICE:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-01-2018,01-15-2018)",one), ("[1,2)","[01-01-2019,01-01-2020)",one)
+NOTICE:  TG_OP = INSERT
+NOTICE:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-01-2018,01-15-2018)",one), ("[1,2)","[01-01-2019,01-01-2020)",one)
+NOTICE:  TG_OP = UPDATE
+NOTICE:  trigger = for_portion_of_test_update_trig, TG_LEVEL = ROW, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one), NEW table = ("[1,2)","[01-15-2018,01-01-2019)",2018-01-15_to_2019-01-01)
+NOTICE:  TG_OP = UPDATE
+NOTICE:  trigger = for_portion_of_test_update_trig_stmt, TG_LEVEL = STATEMENT, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one), NEW table = ("[1,2)","[01-15-2018,01-01-2019)",2018-01-15_to_2019-01-01)
+ROLLBACK;
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+NOTICE:  TG_OP = INSERT
+NOTICE:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-21-2018,01-01-2020)",one)
+NOTICE:  TG_OP = DELETE
+NOTICE:  trigger = for_portion_of_test_delete_trig, TG_LEVEL = ROW, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one)
+NOTICE:  TG_OP = DELETE
+NOTICE:  trigger = for_portion_of_test_delete_trig_stmt, TG_LEVEL = STATEMENT, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one)
+ROLLBACK;
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+NOTICE:  TG_OP = INSERT
+NOTICE:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-02-2018,01-01-2020)",one)
+NOTICE:  TG_OP = UPDATE
+NOTICE:  trigger = for_portion_of_test_update_trig, TG_LEVEL = ROW, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one), NEW table = ("[1,2)","[01-01-2018,01-02-2018)",NULL_to_2018-01-01)
+NOTICE:  TG_OP = UPDATE
+NOTICE:  trigger = for_portion_of_test_update_trig_stmt, TG_LEVEL = STATEMENT, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one), NEW table = ("[1,2)","[01-01-2018,01-02-2018)",NULL_to_2018-01-01)
+ROLLBACK;

 drop view uv_iocu_view;
 drop table uv_iocu_tab;

index 1d8a414eea7..4fd7f3d0c73 100644
 
 # ----------
index 00000000000..1cb907eeb30
@@ -0,0 +1,502 @@
+DROP FUNCTION for_portion_of_trigger CASCADE;
+-- Triggers with a custom transition table name:
+INSERT INTO for_portion_of_test VALUES ('[1,1]', '[2018-01-01,2020-01-01)', 'one');
+CREATE FUNCTION dump_trigger()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+  RAISE NOTICE 'TG_OP = %', TG_OP;
+  IF TG_OP = 'INSERT' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, NEW table = %',
+      TG_NAME, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'UPDATE' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, OLD table = %, NEW table = %',
+      TG_NAME, TG_LEVEL,
+      (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table),
+      (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'DELETE' THEN
+    RAISE NOTICE 'trigger = %, TG_LEVEL = %, OLD table = %',
+      TG_NAME, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
+  RETURN NULL;
+$$;
+CREATE TRIGGER for_portion_of_test_insert_trig
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_insert_trig_stmt
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_update_trig
+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();
+CREATE TRIGGER for_portion_of_test_update_trig_stmt
+AFTER UPDATE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig_stmt
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+  FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01'
+  SET name = '2018-01-15_to_2019-01-01';
+ROLLBACK;
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+ROLLBACK;
+  FOR PORTION OF valid_at FROM NULL TO '2018-01-02'
+  SET name = 'NULL_to_2018-01-01';
+ROLLBACK;

 

 -- temporal UNIQUE:

 

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

---

index 2d371fed40d..32acc8960a6 100644
@@ -9907,7 +9907,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10096,12 +10096,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 
@@ -10111,6 +10114,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12748,11 +12758,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12839,11 +12857,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*
index 7c11b767758..7352df13156 100644

 

 

 

 

 

From cfa8a014a697326db97a21ed2788756d53f4391c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v24 7/7] Add PERIODs

 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  75 ++
 src/backend/commands/tablecmds.c              | 759 +++++++++++++++++-
 src/include/catalog/pg_period.h               |  57 ++
 src/include/catalog/pg_range.h                |   2 +
 src/test/regress/expected/generated.out       |   4 +-
 src/test/regress/expected/periods.out         | 177 ++++
 52 files changed, 2390 insertions(+), 40 deletions(-)

+     <row>

 

index 6905b75bb5c..d4dfe48b2a7 100644

 
+  <para>

   <para>
 
   <para>
index 352ba6d3e2e..3912c6cf344 100644
index e742c78ea35..19bbb9f8cde 100644
@@ -671,6 +673,15 @@ findDependentObjects(const ObjectAddress *object,
 						return;
 					}
 
+					/*
+					 * If a table attribute is an internal part of something else
+					 * (e.g. the GENERATED column used by a PERIOD),
+					 * and we are deleting the whole table,
+					 * then it's okay.
+					 */
+					if (foundDep->objsubid && !object->objectSubId)
+						break;
+
 					/*
 					 * We postpone actually issuing the error message until
 					 * after this loop, so that we can make the behavior
@@ -1443,6 +1454,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveConstraintById(object->objectId);
 			break;
 
+		case OCLASS_PERIOD:
@@ -2862,6 +2877,9 @@ getObjectClass(const ObjectAddress *object)
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index c73f7bcd011..cb85569a57d 100644
@@ -2040,6 +2041,80 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 
+/*

 /*
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case OCLASS_PERIOD:
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
index f193c7ddf60..64f17397a08 100644
index 32acc8960a6..7f5b7fb2fa1 100644

+	/*
 
+	/*
 
 	/*
 
+/*

+/*----------
+ * MergePeriods

 
 /*
 
@@ -7397,14 +7893,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7448,6 +7959,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -8099,6 +8680,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -14170,6 +14900,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 
+			case OCLASS_PERIOD:
+				if (subtype == AT_AlterColumnType)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("cannot alter type of a column used by a period"),
+							 errdetail("%s depends on column \"%s\"",
+									   getObjectDescription(&foundObject, false),
+									   colName)));
+				break;
+
 			case OCLASS_REWRITE:
 				/* XXX someday see if we can cope with revising views */
 				if (subtype == AT_AlterColumnType)
@@ -14234,6 +14974,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -16046,7 +16795,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*

+
 TypedTableElement:

+TablePeriod:

index 56ac4f516ea..7083984e6d9 100644

+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 
 
index 93d97a40900..9270175f3d7 100644
 
@@ -485,6 +489,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
index 9cd8783325c..146919ceb0b 100644

 
+		/* print periods */
 
index 6be76dca1dd..52d9924c5d3 100644
index 3462572eb55..0a3c8111b67 100644
index 00000000000..df9d73a3f54
@@ -0,0 +1,57 @@
+MAKE_SYSCACHE(PERIODOID, pg_period_oid_index, 32);
+MAKE_SYSCACHE(PERIODNAME, pg_period_perrelid_pername_index, 32);
index 0bd3ab688bd..10a94dd11f1 100644
@@ -59,8 +59,10 @@ typedef FormData_pg_range *Form_pg_range;

 MAKE_SYSCACHE(RANGETYPE, pg_range_rngtypid_index, 4);
+MAKE_SYSCACHE(RANGESUBTYPE, pg_range_rngsubtype_rngtypid_index, 4);
 MAKE_SYSCACHE(RANGEMULTIRANGE, pg_range_rngmultitypid_index, 4);
 
 /*

 

 #endif							/* TABLECMDS_H */

+
+/* ----------
 
diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out
index a4f37736623..d9c1f78ed8b 100644
--- a/src/test/regress/expected/generated.out
+++ b/src/test/regress/expected/generated.out
@@ -19,7 +19,9 @@ SELECT table_name, column_name, dependent_column FROM information_schema.column_
  table_name | column_name | dependent_column 
 ------------+-------------+------------------
  gtest1     | a           | b
-(1 row)
+ pt         | de          | p
+ pt         | ds          | p
+(3 rows)
 
 \d gtest1
                             Table "public.gtest1"
index 4fd7f3d0c73..6685cc357df 100644
 
 # ----------

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 25 Jan 2024 07:31:44 +0100

On 24.01.24 23:06, Paul Jungwirth wrote:
> Thanks Peter! I noticed the comment on gist_stratnum_btree was 
> out-of-date, so here is a tiny patch correcting it.

committed that

---

From: jian he <[email-masked]>
Date: Mon, 29 Jan 2024 08:00:00 +0800

I fixed your tests, some of your tests can be simplified, (mainly
primary key constraint is unnecessary for the failed tests)
also your foreign key patch test table, temporal_rng is created at
line 141, and we use it at around line 320.
it's hard to get the definition of temporal_rng.  I drop the table
and recreate it.
So people can view the patch with tests more easily.

+         <para>

in v24-0003-Add-temporal-FOREIGN-KEYs.patch
 <literal>FOR PORTION OF</literal> not yet implemented, so we should
not mention it.

+     <para>
+      If the last column is marked with <literal>PERIOD</literal>,
+      it must be a period or range column, and the referenced table
+      must have a temporal primary key.
can we change "it must be a period or range column" to "it must be a
range column", maybe we can add it on another patch.

---

From: Isaac Morland <[email-masked]>
Date: Tue, 21 May 2024 14:27:29 -0400

On Tue, 21 May 2024 at 13:57, Robert Haas <[email-masked]> wrote:

What I think is less clear is what that means for temporal primary
> keys. As Paul pointed out upthread, in every other case, a temporal
> primary key is at least as unique as a regular primary key, but in
> this case, it isn't. And someone might reasonably think that a
> temporal primary key should exclude empty ranges just as all primary
> keys exclude nulls. Or they might think the opposite.
>

Fascinating. I think you're absolutely right that it's clear that two empty
intervals don't conflict. If somebody wants to claim two intervals
conflict, they need to point to at least one instant in time that is common
between them.

But a major point of a primary key, it seems to me, is that it uniquely
identifies a row. If items are identified by a time range, non-overlapping
or not, then the empty range can only identify one item (per value of
whatever other columns are in the primary key). I think for a unique key
the non-overlapping restriction has to be considered an additional
restriction on top of the usual uniqueness restriction.

I suspect in many applications there will be a non-empty constraint; for
example, it seems quite reasonable to me for a meeting booking system to
forbid empty meetings. But when they are allowed they should behave in the
mathematically appropriate way.

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 5 Jun 2024 13:56:15 -0700

On 5/21/24 11:27, Isaac Morland wrote:
> On Tue, 21 May 2024 at 13:57, Robert Haas <[email-masked] <mailto:[email-masked]>> wrote:
> 
> Fascinating. I think you're absolutely right that it's clear that two empty intervals don't 
> conflict. If somebody wants to claim two intervals conflict, they need to point to at least one 
> instant in time that is common between them.
> 
> But a major point of a primary key, it seems to me, is that it uniquely identifies a row. If items 
> are identified by a time range, non-overlapping or not, then the empty range can only identify one 
> item (per value of whatever other columns are in the primary key). I think for a unique key the 
> non-overlapping restriction has to be considered an additional restriction on top of the usual 
> uniqueness restriction.
> 
> I suspect in many applications there will be a non-empty constraint; for example, it seems quite 
> reasonable to me for a meeting booking system to forbid empty meetings. But when they are allowed 
> they should behave in the mathematically appropriate way.

Finding a way forward for temporal PKs got a lot of discussion at pgconf.dev (thanks especially to 
Peter Eisentraut and Jeff Davis!), so I wanted to summarize some options and describe what I think 
is the best approach.

First the problem: empty ranges! A temporal PK/UNIQUE constraint is basically an exclusion 
constraint that is `(id WITH =, valid_at WITH &&)`. But the special 'empty' value never overlaps 
anything, *including itself*. (Note it has no "position": [3,3) is the same as [4,4).) Since the 
exclusion constraint forbids overlapping ranges, and empties never overlap, your table can have 
duplicates. (I'm talking about "literal uniqueness" as discussed in [1].) For instance:

     CREATE EXTENSION btree_gist;
     CREATE TABLE t (id int, valid_at daterange, name text);
     ALTER TABLE t ADD CONSTRAINT tpk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
     INSERT INTO t VALUES (1, 'empty', 'foo');
     INSERT INTO t VALUES (1, 'empty', 'bar');

Multiranges have the same problem. So what do we do about that?

**Option 0**: Allow it but document it. It shouldn't happen in practice: there is no reason for an 
empty range to get into a temporal table, and it arguably doesn't mean anything. The record is true 
at no time? But of course it will happen anyway. It's a footgun and will break expectations for at 
least some.

It causes problems for us too. If you say `SELECT name FROM t GROUP BY id, valid_at`, we recognize 
that `name` is a functional dependency on the PK, so we allow it and give you the first row matching 
each key. You might get "foo" or you might get "bar". Also the planner uses not-nullable uniqueness 
to take many shortcuts. I couldn't create any concrete breakage there, but I bet someone else could. 
PKs that are not literally unique seems like something that would cause headaches for years.

**Option 1**: Temporal PKs should automatically create a CHECK constraint that forbids empty ranges. 
Should UNIQUE constraints too? I'm tempted to say no, since sometimes users surprise us by coming up 
with new ways to use things. For instance one way to use empty ranges is to reference a temporal 
table from a non-temporal table, since `'empty' <@ anything` is always true (though this has 
questionable meaning or practical use). But probably we should forbid empties for UNIQUE constraints 
too. Forbidding them is more aligned with the SQL standard, which says that when you have a PERIOD, 
startcol < endcol (not <=). And it feels more consistent to treat both constraints the same way. 
Finally, if UNIQUEs do allow empties, we still risk confusing our planner.

My last patch created these CHECK constraints for PKs (but not UNIQUEs) as INTERNAL dependencies. 
It's pretty clunky. There are lots of cases to handle, e.g. `ALTER COLUMN c TYPE` may reuse the PK 
index or may generate a new one. And what if the user already created the same constraint? Seeing 
all the trouble giving PKs automatic (cataloged) NOT NULL constraints makes me wary about this 
approach. It's not as bad, since there is no legacy, but it's still more annoying than I expected.

Finally, hanging the CHECK constraint off the PK sets us up for problems when we add true PERIODs. 
Under 11.27 of SQL/Foundation, General Rules 2b says that defining a PERIOD should automatically add 
a CHECK constraint that startcol < endcol. That is already part of my last patch in this series. But 
that would be redundant with the constraint from the PK. And attaching the constraint to the PERIOD 
is a lot simpler than attaching it to the PK.

**Option 2**: Add a new operator, called &&&, that works like && except an empty range *does* 
overlap another empty range. Empty ranges should still not overlap anything else. This would fix the 
exclusion constraint. You could add `(5, 'empty')` once but not twice. This would allow empties to 
people who want to use them. (We would still forbid them if you define a PERIOD, because those come 
with the CHECK constraint mentioned above.)
And there is almost nothing to code. But it is mathematically suspect to say an empty range overlaps 
something small (something with zero width) but not something big. Surely if a && b and b <@ c, then 
a && c? So this feels like the kind of elegant hack that you eventually regret.

**Option 3**: Forbid empties, not as a reified CHECK constraint, but just with some code in the 
executor. Again we could do just PKs or PKs and UNIQUEs. Let's do both, for all the reasons above. 
Not creating a CHECK constraint is much less clunky. There is no catalog entry to create/drop. Users 
don't wonder where it came from when they say `\d t`. It can't conflict with constraints of their 
own. We would enforce this in ExecConstraints, where we enforce NOT NULL and CHECK constraints, for 
any table with constraints where conperiod is true. We'd also need to do this check on existing rows 
when you create a temporal PK/UQ. This option also requires a new field in pg_class: just as we have 
relchecks, relhasrules, relhastriggers, etc. to let us skip work in the relcache, I assume we'd want 
relperiods.

**Option 4**: Teach GiST indexes to enforce uniqueness. We didn't discuss this at pgconf, at least 
not in reference to the empties problem. But I was thinking about this request from Matthias for 
temporal PKs & UQs to support `USING INDEX idx`.[2] It is confusing that a temporal index has 
indisunique, but if you try to create a unique GiST index directly we say they don't support UNIQUE 
indexes! Similarly `pg_indexam_has_property(783, 'can_unique')` returns false. There is something 
muddled about all that. So how about we give the GiST AM handler amcanunique?

As I understand it, GiST indexes are capable of uniqueness,[3] and indeed today you can create an 
exclusion constraint with the same effect, but in the past the core had no way of asking an opclass 
which operator gave equality. With the stratnum support proc from 6db4598fcb (part of this patch 
series, but reverted from v17), we could get a known operator for "equals". If the index's opclasses 
had that sproc and it gave non-zero for RTEqualStrategyNumber, then CREATE UNIQUE INDEX would 
succeed. We would just ("just") need to make GiST raise an error if it found a duplicate. And if 
*that* was happening, the empty ranges wouldn't cause a problem.

I think Option 3 is good, but I like Option 4 a lot because (1) it doesn't assume ranges & 
multiranges (2) it allows empties if users have some reason for them (3) since the real problem is 
duplicates, forbidding them is a more precise solution, (4) it clears up the confusing situation of 
GiST not being canunique, even though you can create an index with indisunique.

OTOH it is probably more work, and it is slower than just forbidding duplicates. (The unique check 
requires a separate index search, according to [3], as an exclusion constraint would do.) Also if we 
do it to make GiST be canunique, that can happen separately from the temporal work.

So I'm proceeding with Option 3, which at worst can eventually become an optimization for Option 4. 
I don't think forbidding empty ranges is a great loss to be honest. But if anyone has any feedback, 
please share: ojections, alternatives, advice---all is welcome.

[1] 
https://www.postgresql.org/message-id/47550967-260b-4180-9791-b224859fe63e%40illuminatedcomputing.com
[2] 
https://www.postgresql.org/message-id/CAEze2Wh21V66udM8cbvBBsAgyQ_5x9nfR0d3sWzbmZk%2B%2Bey7xw%40mail.gmail.com
[3] https://dsf.berkeley.edu/papers/sigmod97-gist.pdf

Yours,

---

From: Robert Haas <[email-masked]>
Date: Wed, 12 Jun 2024 10:31:49 -0400

> a && c? So this feels like the kind of elegant hack that you eventually regret.

I think this might be fine.

> relchecks, relhasrules, relhastriggers, etc. to let us skip work in the relcache, I assume we'd want

I don't really like the existing relhasWHATEVER fields and am not very
keen about adding more of them. Maybe it will turn out to be the best
way, but finding the right times to set and unset such fields has been
challenging over the years, and we've had to fix some bugs. So, if you
go this route, I recommend looking carefully at whether there's a
reasonable way to avoid the need for such a field. Other than that,
this idea seems reasonable.

> *that* was happening, the empty ranges wouldn't cause a problem.

Isn't this just a more hacky version of option (2)?

-- 
Robert Haas
EDB: http://www.enterprisedb.com

---

From: jian he <[email-masked]>
Date: Tue, 9 Jul 2024 15:15:39 +0800

> muddled about all that. So how about we give the GiST AM handler amcanunique?
I think we can Forbid empties,not not mess with pg_class.

to make the communication smooth, i've set the base commit to
46a0cd4cefb4d9b462d8cc4df5e7ecdd190bea92
{Add temporal PRIMARY KEY and UNIQUE constraints}
https://git.postgresql.org/cgit/postgresql.git/commit/?id=46a0cd4cefb4d9b462d8cc4df5e7ecdd190bea92
you can git reset --hard 46a0cd4cefb4d9b462d8cc4df5e7ecdd190bea92
then apply the attached patch.

I hope I understand it correctly.
previously revert is only because the special value: empty.
i tried to use the operator &&&, new gist strategy number, pg_amop
entry to solve the problem.
Now with the applied patch, if the range column is specified WITHOUT OVERLAPS,
then this column is not allowed to have any empty range value.

logic work through:
* duplicate logic of range_overlaps but disallow empty value. also
have the operator &&&, (almost equivalent to &&)
* add new gist strategy number
* thanks to  add stratnum GiST support function
(https://git.postgresql.org/cgit/postgresql.git/commit/?id=6db4598fcb82a87a683c4572707e522504830a2b)
now we can set the strategy number to the mapped new function
(equivalent to range_overlaps, but error out empty value)
* in ComputeIndexAttrs, set the strategy number to the newly created
StrategyNumber in "else if (iswithoutoverlaps)" block.
* Similarly refactor src/backend/utils/adt/rangetypes_gist.c make the
index value validation using newly created function.

function name, error message maybe not great now, but it works.
------full demo, also see the comments.
DROP TABLE if exists temporal_rng;
CREATE TABLE temporal_rng (id int4range, valid_at tsrange);
ALTER TABLE temporal_rng
ADD CONSTRAINT temporal_rng_pk
PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
--should be fine.
INSERT INTO temporal_rng VALUES ('empty', '[2022-01-01,2022-01-02]');
--will error out, period column, empty range not allowed
INSERT INTO temporal_rng VALUES ('[3,3]', 'empty');

ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
--period constraint dropped, now should be fine.
INSERT INTO temporal_rng VALUES ('[3,3]', 'empty');

--reinstall constraint, should error out
--because existing one row has empty value.
ALTER TABLE temporal_rng
ADD CONSTRAINT temporal_rng_pk
PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
delete from temporal_rng where id = '[3,3]';

--reinstall constraint, should be fine, because empty value removed.
ALTER TABLE temporal_rng
ADD CONSTRAINT temporal_rng_pk
PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
From d8721a13a2575f2e74841168b12dc018c90a0c66 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 9 Jul 2024 15:14:10 +0800
Subject: [PATCH v1 1/1] forbid empty range value for period column.

if one column have WITHOUT OVERLAPS attribute,
then forbid this column to have 'empty' range value.
---
 src/backend/commands/indexcmds.c              |  4 +-
 src/backend/utils/adt/multirangetypes.c       | 35 ++++++++++++++
 src/backend/utils/adt/rangetypes.c            | 46 +++++++++++++++++++
 src/backend/utils/adt/rangetypes_gist.c       |  8 ++++
 src/include/access/stratnum.h                 |  3 +-
 src/include/catalog/pg_amop.dat               |  6 +++
 src/include/catalog/pg_operator.dat           | 11 +++++
 src/include/catalog/pg_proc.dat               |  8 ++++
 src/include/utils/multirangetypes.h           |  4 ++
 src/include/utils/rangetypes.h                |  3 ++
 src/test/regress/expected/opr_sanity.out      |  6 ++-
 .../regress/expected/without_overlaps.out     |  2 +
 src/test/regress/sql/without_overlaps.sql     |  2 +
 13 files changed, 133 insertions(+), 5 deletions(-)

diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 7a87626f..acce7378 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2178,7 +2178,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 			Oid			opid;
 
 			if (attn == nkeycols - 1)
-				strat = RTOverlapStrategyNumber;
+				strat = RTPeriodOverlapStrategyNumber;
 			else
 				strat = RTEqualStrategyNumber;
 			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
@@ -2441,7 +2441,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 	Oid			opcintype;
 	StrategyNumber instrat = *strat;
 
-	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber || instrat == RTPeriodOverlapStrategyNumber);
 
 	*opid = InvalidOid;
 
diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index f82e6f42..d30d6291 100644
--- a/src/backend/utils/adt/multirangetypes.c
+++ b/src/backend/utils/adt/multirangetypes.c
@@ -1969,6 +1969,19 @@ multirange_overlaps_multirange(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(multirange_overlaps_multirange_internal(typcache->rngtype, mr1, mr2));
 }
 
+
+Datum
+period_multirange_overlaps_multirange(PG_FUNCTION_ARGS)
+{
+	MultirangeType *mr1 = PG_GETARG_MULTIRANGE_P(0);
+	MultirangeType *mr2 = PG_GETARG_MULTIRANGE_P(1);
+	TypeCacheEntry *typcache;
+
+	typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr1));
+
+	PG_RETURN_BOOL(multirange_overlaps_multirange_internal(typcache->rngtype, mr1, mr2));
+}
+
 /*
  * Comparison function for checking if any range of multirange overlaps given
  * key range using binary search.
@@ -1990,6 +2003,28 @@ multirange_range_overlaps_bsearch_comparison(TypeCacheEntry *typcache,
 	return 0;
 }
 
+bool
+period_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
+								   const RangeType *r,
+								   const MultirangeType *mr)
+{
+	RangeBound	bounds[2];
+	bool		empty;
+
+	/*
+	 * Empties never overlap, even with empties. (This seems strange since
+	 * they *do* contain each other, but we want to follow how ranges work.)
+	 */
+	if (RangeIsEmpty(r) || MultirangeIsEmpty(mr))
+		elog(ERROR, "multirange types cannot be empty in this case");
+
+	range_deserialize(rangetyp, r, &bounds[0], &bounds[1], &empty);
+	Assert(!empty);
+
+	return multirange_bsearch_match(rangetyp, mr, bounds,
+									multirange_range_overlaps_bsearch_comparison);
+}
+
 bool
 range_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
 								   const RangeType *r,
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 2d94a6b8..42f72341 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -869,6 +869,39 @@ range_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1, const Ran
 	return false;
 }
 
+/* overlaps? (internal version) */
+bool
+period_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2)
+{
+	RangeBound	lower1,
+				lower2;
+	RangeBound	upper1,
+				upper2;
+	bool		empty1,
+				empty2;
+
+	/* Different types should be prevented by ANYRANGE matching rules */
+	if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2))
+		elog(ERROR, "range types do not match");
+
+	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+	/* An empty range does not overlap any other range */
+	if (empty1 || empty2)
+		elog(ERROR, "period cannot be empty");
+
+	if (range_cmp_bounds(typcache, &lower1, &lower2) >= 0 &&
+		range_cmp_bounds(typcache, &lower1, &upper2) <= 0)
+		return true;
+
+	if (range_cmp_bounds(typcache, &lower2, &lower1) >= 0 &&
+		range_cmp_bounds(typcache, &lower2, &upper1) <= 0)
+		return true;
+
+	return false;
+}
+
 /* overlaps? */
 Datum
 range_overlaps(PG_FUNCTION_ARGS)
@@ -882,6 +915,19 @@ range_overlaps(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(range_overlaps_internal(typcache, r1, r2));
 }
 
+/* period (range that does have empty range value) overlaps? */
+Datum
+period_overlaps(PG_FUNCTION_ARGS)
+{
+	RangeType  *r1 = PG_GETARG_RANGE_P(0);
+	RangeType  *r2 = PG_GETARG_RANGE_P(1);
+	TypeCacheEntry *typcache;
+
+	typcache = range_get_typcache(fcinfo, RangeTypeGetOid(r1));
+
+	PG_RETURN_BOOL(period_overlaps_internal(typcache, r1, r2));
+}
+
 /* does not extend to right of? (internal version) */
 bool
 range_overleft_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2)
diff --git a/src/backend/utils/adt/rangetypes_gist.c b/src/backend/utils/adt/rangetypes_gist.c
index cb28e985..ac60def4 100644
--- a/src/backend/utils/adt/rangetypes_gist.c
+++ b/src/backend/utils/adt/rangetypes_gist.c
@@ -929,6 +929,8 @@ range_gist_consistent_int_range(TypeCacheEntry *typcache,
 			return (!range_after_internal(typcache, key, query));
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_internal(typcache, key, query);
+		case PERIODSTRAT_OVERLAPS:
+			return period_overlaps_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			if (RangeIsEmpty(key) || RangeIsEmpty(query))
 				return false;
@@ -989,6 +991,8 @@ range_gist_consistent_int_multirange(TypeCacheEntry *typcache,
 			if (RangeIsEmpty(key) || MultirangeIsEmpty(query))
 				return false;
 			return (!range_after_multirange_internal(typcache, key, query));
+		case PERIODSTRAT_OVERLAPS:
+			return period_overlaps_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
@@ -1066,6 +1070,8 @@ range_gist_consistent_leaf_range(TypeCacheEntry *typcache,
 			return range_before_internal(typcache, key, query);
 		case RANGESTRAT_OVERLEFT:
 			return range_overleft_internal(typcache, key, query);
+		case PERIODSTRAT_OVERLAPS:
+			return period_overlaps_internal(typcache, key, query);
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
@@ -1103,6 +1109,8 @@ range_gist_consistent_leaf_multirange(TypeCacheEntry *typcache,
 			return range_overleft_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_multirange_internal(typcache, key, query);
+		case PERIODSTRAT_OVERLAPS:
+			return period_overlaps_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			return range_overright_multirange_internal(typcache, key, query);
 		case RANGESTRAT_AFTER:
diff --git a/src/include/access/stratnum.h b/src/include/access/stratnum.h
index 8a47d3c9..94737af4 100644
--- a/src/include/access/stratnum.h
+++ b/src/include/access/stratnum.h
@@ -78,8 +78,9 @@ typedef uint16 StrategyNumber;
 #define RTPrefixStrategyNumber			28	/* for text ^@ */
 #define RTOldBelowStrategyNumber		29	/* for old spelling of <<| */
 #define RTOldAboveStrategyNumber		30	/* for old spelling of |>> */
+#define RTPeriodOverlapStrategyNumber   31	/* for special gist &&& */
 
-#define RTMaxStrategyNumber				30
+#define RTMaxStrategyNumber				31

 #endif							/* STRATNUM_H */
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index d8a05214..96590c32 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1358,6 +1358,9 @@
 { amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '3',
   amopopr => '&&(anyrange,anyrange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
+  amoprighttype => 'anyrange', amopstrategy => '31',
+  amopopr => '&&&(anyrange,anyrange)', amopmethod => 'gist' },
 { amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anymultirange', amopstrategy => '3',
   amopopr => '&&(anyrange,anymultirange)', amopmethod => 'gist' },
@@ -1414,6 +1417,9 @@
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
   amoprighttype => 'anymultirange', amopstrategy => '3',
   amopopr => '&&(anymultirange,anymultirange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
+  amoprighttype => 'anymultirange', amopstrategy => '31',
+  amopopr => '&&&(anymultirange,anymultirange)', amopmethod => 'gist' },
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
   amoprighttype => 'anyrange', amopstrategy => '3',
   amopopr => '&&(anymultirange,anyrange)', amopmethod => 'gist' },
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 0e7511dd..4022b9f7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3095,6 +3095,11 @@
   oprresult => 'bool', oprcom => '&&(anyrange,anyrange)',
   oprcode => 'range_overlaps', oprrest => 'rangesel',
   oprjoin => 'areajoinsel' },
+{ oid => '4552', oid_symbol => 'OID_PERIOD_OVERLAP_OP', descr => 'period overlaps',
+  oprname => '&&&', oprleft => 'anyrange', oprright => 'anyrange',
+  oprresult => 'bool', oprcom => '&&&(anyrange,anyrange)',
+  oprcode => 'period_overlaps', oprrest => 'rangesel',
+  oprjoin => 'areajoinsel' },
 { oid => '3889', oid_symbol => 'OID_RANGE_CONTAINS_ELEM_OP',
   descr => 'contains',
   oprname => '@>', oprleft => 'anyrange', oprright => 'anyelement',
@@ -3314,6 +3319,12 @@
   oprresult => 'bool', oprcom => '&&(anymultirange,anymultirange)',
   oprcode => 'multirange_overlaps_multirange', oprrest => 'multirangesel',
   oprjoin => 'areajoinsel' },
+{ oid => '4553', oid_symbol => 'PERIOD_OID_MULTIRANGE_OVERLAPS_MULTIRANGE_OP',
+  descr => 'overlaps',
+  oprname => '&&&', oprleft => 'anymultirange', oprright => 'anymultirange',
+  oprresult => 'bool', oprcom => '&&&(anymultirange,anymultirange)',
+  oprcode => 'period_multirange_overlaps_multirange', oprrest => 'multirangesel',
+  oprjoin => 'areajoinsel' },
 { oid => '2869', oid_symbol => 'OID_MULTIRANGE_CONTAINS_ELEM_OP',
   descr => 'contains',
   oprname => '@>', oprleft => 'anymultirange', oprright => 'anyelement',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e4115cd0..a876842f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10506,6 +10506,10 @@
 { oid => '3857',
   proname => 'range_overlaps', prorettype => 'bool',
   proargtypes => 'anyrange anyrange', prosrc => 'range_overlaps' },
+{ oid => '4551',
+  proname => 'period_overlaps', prorettype => 'bool',
+  proargtypes => 'anyrange anyrange', prosrc => 'period_overlaps' },
+
 { oid => '3858',
   proname => 'range_contains_elem', prosupport => 'range_contains_elem_support',
   prorettype => 'bool', proargtypes => 'anyrange anyelement',
@@ -10749,6 +10753,10 @@
   proname => 'multirange_overlaps_multirange', prorettype => 'bool',
   proargtypes => 'anymultirange anymultirange',
   prosrc => 'multirange_overlaps_multirange' },
+{ oid => '4554',
+  proname => 'period_multirange_overlaps_multirange', prorettype => 'bool',
+  proargtypes => 'anymultirange anymultirange',
+  prosrc => 'period_multirange_overlaps_multirange' },
 { oid => '4249',
   proname => 'multirange_contains_elem', prorettype => 'bool',
   proargtypes => 'anymultirange anyelement',
diff --git a/src/include/utils/multirangetypes.h b/src/include/utils/multirangetypes.h
index 84525439..3c6032fa 100644
--- a/src/include/utils/multirangetypes.h
+++ b/src/include/utils/multirangetypes.h
@@ -89,6 +89,10 @@ extern bool range_contains_multirange_internal(TypeCacheEntry *rangetyp,
 extern bool multirange_contains_multirange_internal(TypeCacheEntry *rangetyp,
 													const MultirangeType *mr1,
 													const MultirangeType *mr2);
+extern bool period_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
+											   const RangeType *r,
+											   const MultirangeType *mr);
+
 extern bool range_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
 											   const RangeType *r,
 											   const MultirangeType *mr);
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 2b574873..7817abf3 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -97,6 +97,7 @@ RangeTypePGetDatum(const RangeType *X)
 #define RANGESTRAT_BEFORE				RTLeftStrategyNumber
 #define RANGESTRAT_OVERLEFT				RTOverLeftStrategyNumber
 #define RANGESTRAT_OVERLAPS				RTOverlapStrategyNumber
+#define PERIODSTRAT_OVERLAPS			RTPeriodOverlapStrategyNumber
 #define RANGESTRAT_OVERRIGHT			RTOverRightStrategyNumber
 #define RANGESTRAT_AFTER				RTRightStrategyNumber
 #define RANGESTRAT_ADJACENT				RTSameStrategyNumber
@@ -126,6 +127,8 @@ extern bool range_after_internal(TypeCacheEntry *typcache, const RangeType *r1,
 								 const RangeType *r2);
 extern bool range_adjacent_internal(TypeCacheEntry *typcache, const RangeType *r1,
 									const RangeType *r2);
+extern bool period_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1,
+									const RangeType *r2);
 extern bool range_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1,
 									const RangeType *r2);
 extern bool range_overleft_internal(TypeCacheEntry *typcache, const RangeType *r1,
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7610b011..8217ad0a 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1147,6 +1147,7 @@ ORDER BY 1, 2;
  #    | #
  &    | &
  &&   | &&
+ &&&  | &&&
  *    | *
  *<   | *>
  *<=  | *>=
@@ -1173,7 +1174,7 @@ ORDER BY 1, 2;
  ~<=~ | ~>=~
  ~<~  | ~>~
  ~=   | ~=
-(29 rows)
+(30 rows)
 
 -- Likewise for negator pairs.
 SELECT DISTINCT o1.oprname AS op1, o2.oprname AS op2
@@ -2008,6 +2009,7 @@ ORDER BY 1, 2, 3;
         783 |           28 | <@
         783 |           29 | <^
         783 |           30 | >^
+        783 |           31 | &&&
         783 |           48 | <@
         783 |           68 | <@
        2742 |            1 | &&
@@ -2088,7 +2090,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(124 rows)
+(125 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 726e9410..ac9cb412 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -289,6 +289,8 @@ DETAIL:  Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:
 INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
 ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
 DETAIL:  Failing row contains ([3,4), null).
+INSERT INTO temporal_rng VALUES ('[3,3]', 'empty');
+ERROR:  period cannot be empty
 --
 -- test a range with both a PK and a UNIQUE constraint
 --
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index c8e8ab99..658fa5fb 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -214,6 +214,8 @@ INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
 INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
 INSERT INTO temporal_rng VALUES ('[3,3]', NULL);
 
+INSERT INTO temporal_rng VALUES ('[3,3]', 'empty');
+
 --
 -- test a range with both a PK and a UNIQUE constraint
 --
-- 
2.34.1

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 17 Jul 2024 20:34:42 -0700

> index value validation using newly created function.

I like this approach a lot, but I'd like to hear what some other people think?

Jian he's &&& operator is similar to what I proposed upthread, but when either operand is an empty 
value it simply raises an error. (It should be an ereport, not an elog, and I think 
multirange_overlaps_multirange_internal is missing the empty check, but I can clean things up when I 
integrate it into the patch series.)

This is much simpler than everything I'm doing: checking for empties in the executor phase, adding a 
field to pg_class, setting things in the relcache, and checking for empties in existing rows when 
you add an index. This patch uses existing infrastructure to do all the work. It seems like a much 
cleaner solution.

Unlike my proposed &&& operator, it doesn't have weird mathematical consequences.

At first I thought raising an error was not great, but it's the same thing you get when you divide 
by zero. It's fine for an operator to have a restricted domain of inputs. And we would only use this 
internally for primary keys and unique constraints, where indeed raising an error is just what we want.

If I don't hear objections (or think of something myself :-), I'm inclined to use this approach.

But what do people think?

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 18 Jul 2024 11:39:09 -0700

> integrate it into the patch series.)

I thought of a possible problem: this operator works great if there are already rows in the table, 
but what if the *first row you insert* has an empty range? Then there is nothing to compare against, 
so the operator will never be used. Right?

Except when I test it, it still works! After running `make installcheck`, I did this:

regression=# truncate temporal_rng cascade;
NOTICE:  truncate cascades to table "temporal_fk_rng2rng"
TRUNCATE TABLE
regression=# insert into temporal_rng values ('[1,2)', 'empty');
ERROR:  range cannot be empty

My mental model must be wrong. Can anyone explain what is happening there? Is it something we can 
depend on?

So I swapped in the &&& patch, cleaned it up, and added tests. But something is wrong. After I get 
one failure from an empty, I keep getting failures, even though the table is empty:

regression=# truncate temporal_rng cascade;
NOTICE:  truncate cascades to table "temporal_fk_rng2rng"
TRUNCATE TABLE
regression=# insert into temporal_rng values ('[1,2)', '[2000-01-01,2010-01-01)'); -- ok so far
INSERT 0 1
regression=# insert into temporal_rng values ('[1,2)', 'empty'); -- should fail and does
ERROR:  range cannot be empty
regression=# insert into temporal_rng values ('[1,2)', '[2010-01-01,2020-01-01)'); -- uh oh
ERROR:  range cannot be empty
regression=# truncate temporal_rng cascade;
NOTICE:  truncate cascades to table "temporal_fk_rng2rng"
TRUNCATE TABLE
regression=# insert into temporal_rng values ('[1,2)', '[2000-01-01,2010-01-01)'); -- ok so far
INSERT 0 1
regression=# insert into temporal_rng values ('[1,2)', '[2010-01-01,2020-01-01)'); -- ok now
INSERT 0 1

It looks like the index is getting corrupted. Continuing from the above:

regression=# create extension pageinspect;
CREATE EXTENSION
regression=# select gist_page_items(get_raw_page('temporal_rng_pk', 0), 'temporal_rng_pk');
                               gist_page_items
----------------------------------------------------------------------------
  (1,"(0,1)",40,f,"(id, valid_at)=(""[1,2)"", ""[2000-01-01,2010-01-01)"")")
  (2,"(0,2)",40,f,"(id, valid_at)=(""[1,2)"", ""[2010-01-01,2020-01-01)"")")
(2 rows)

regression=# insert into temporal_rng values ('[1,2)', 'empty');
ERROR:  range cannot be empty
regression=# select gist_page_items(get_raw_page('temporal_rng_pk', 0), 'temporal_rng_pk');
                               gist_page_items
----------------------------------------------------------------------------
  (1,"(0,1)",40,f,"(id, valid_at)=(""[1,2)"", ""[2000-01-01,2010-01-01)"")")
  (2,"(0,2)",40,f,"(id, valid_at)=(""[1,2)"", ""[2010-01-01,2020-01-01)"")")
  (3,"(0,3)",32,f,"(id, valid_at)=(""[1,2)"", empty)")
(3 rows)

So maybe this is a bad place to ereport? Or is this a deeper bug with GiST? Here is where we're 
doing it:

#0  range_nonempty_overlaps_internal (typcache=0x635a7fbf67f0, r1=0x635a7fc11f20, r2=0x635a7fc11f40) 
at rangetypes.c:876
#1  0x0000635a7f06175d in range_gist_consistent_leaf_range (typcache=0x635a7fbf67f0, strategy=31, 
key=0x635a7fc11f20, query=0x635a7fc11f40)
     at rangetypes_gist.c:1076
#2  0x0000635a7f05fc9a in range_gist_consistent (fcinfo=0x7ffcd20f9f60) at rangetypes_gist.c:216
#3  0x0000635a7f12d780 in FunctionCall5Coll (flinfo=0x635a7fb44eb8, collation=0, 
arg1=140723832725648, arg2=109240340727454, arg3=31, arg4=0,
     arg5=140723832725567) at fmgr.c:1242
#4  0x0000635a7e999af6 in gistindex_keytest (scan=0x635a7fb44d50, tuple=0x7d155c0a3fd0, 
page=0x7d155c0a2000 "", offset=1, recheck_p=0x7ffcd20fa129,
     recheck_distances_p=0x7ffcd20fa12a) at gistget.c:221
#5  0x0000635a7e99a109 in gistScanPage (scan=0x635a7fb44d50, pageItem=0x7ffcd20fa1e0, 
myDistances=0x0, tbm=0x0, ntids=0x0) at gistget.c:436
#6  0x0000635a7e99a797 in gistgettuple (scan=0x635a7fb44d50, dir=ForwardScanDirection) at gistget.c:637
#7  0x0000635a7e9e4d38 in index_getnext_tid (scan=0x635a7fb44d50, direction=ForwardScanDirection) at 
indexam.c:590
#8  0x0000635a7e9e4f7d in index_getnext_slot (scan=0x635a7fb44d50, direction=ForwardScanDirection, 
slot=0x635a7fb44950) at indexam.c:682
#9  0x0000635a7ec5690b in check_exclusion_or_unique_constraint (heap=0x7d1560cea348, 
index=0x7d1560cedd98, indexInfo=0x635a7fb44c40, tupleid=0x635a7fb44580,
     values=0x7ffcd20faf00, isnull=0x7ffcd20faee0, estate=0x635a7fb434a0, newIndex=false, 
waitMode=CEOUC_WAIT, violationOK=false, conflictTid=0x0)
     at execIndexing.c:780
#10 0x0000635a7ec55c58 in ExecInsertIndexTuples (resultRelInfo=0x635a7fb43930, slot=0x635a7fb44550, 
estate=0x635a7fb434a0, update=false, noDupErr=false,
     specConflict=0x0, arbiterIndexes=0x0, onlySummarizing=false) at execIndexing.c:483
#11 0x0000635a7eca38a2 in ExecInsert (context=0x7ffcd20fb1b0, resultRelInfo=0x635a7fb43930, 
slot=0x635a7fb44550, canSetTag=true, inserted_tuple=0x0,
     insert_destrel=0x0) at nodeModifyTable.c:1145

Is there anything I can do to save this &&& idea? I've attached the patches I'm working with, 
rebased to cd85ae1114.

If ereport just won't work, then I might explore other definitions of a &&& operator. It was really 
nice to have such a clean solution.

Yours,

From bb3ce26d67e3dea7587b81390e884325d9e2f49e Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 19 Jan 2024 15:41:44 +0100
Subject: [PATCH v36 1/3] Add stratnum GiST support function

This is support function 12 for the GiST AM and translates
"well-known" RT*StrategyNumber values into whatever strategy number is
used by the opclass (since no particular numbers are actually
required).  We will use this to support temporal PRIMARY
KEY/UNIQUE/FOREIGN KEY/FOR PORTION OF functionality.

updates btree_gist from 1.7 to 1.8, adding the support function for
all its opclasses.
 contrib/btree_gist/btree_gist.c              | 27 ++++++
 16 files changed, 272 insertions(+), 7 deletions(-)

index 073dcc745c4..9ab8548bc0e 100644

 
index 92520aedae1..5fd4cce27d0 100644

 PG_MODULE_MAGIC;
 
@@ -51,3 +53,28 @@ gbt_decompress(PG_FUNCTION_ARGS)
 
index 39c7bf370d6..f789824c83b 100644
@@ -266,7 +266,7 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and six that are optional.
+   <acronym>GiST</acronym> must provide, and seven that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -289,6 +289,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    user-specified parameters.
    The optional eleventh method <function>sortsupport</function> is used to
    speed up building a <acronym>GiST</acronym> index.
+   The optional twelfth method <function>stratnum</function> is used to
+   translate well-known <literal>RT*StrategyNumber</literal>s (from
+   <filename>src/include/access/stratnum.h</filename>) into strategy numbers
+   used by the operator class.
  </para>
 
  <variablelist>
@@ -1163,6 +1167,65 @@ my_sortsupport(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><function>stratnum</function></term>
+     <listitem>
+      <para>
+       Given an <literal>RT*StrategyNumber</literal> value from
+       <filename>src/include/access/stratnum.h</filename>, returns a strategy
+       number used by this operator class for matching functionality.  The
+       function should return <literal>InvalidStrategy</literal> if the
+       operator class has no matching strategy.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this:
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_stratnum(integer)
+RETURNS integer
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this skeleton:
+
+<programlisting>
+PG_FUNCTION_INFO_V1(my_stratnum);
+
+Datum
+my_stratnum(PG_FUNCTION_ARGS)
+{
+    StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(1);
+    StrategyNumber ret = InvalidStrategy;
+
+    switch (strategy)
+    {
+        case RTEqualStrategyNumber:
+            ret = BTEqualStrategyNumber;
+    }
+
+    PG_RETURN_UINT16(ret);
+}
+</programlisting>
+      </para>
+
+      <para>
+       One translation function is provided by
+       <productname>PostgreSQL</productname>:
+       <literal>gist_stratnum_identity</literal> is for operator classes that
+       already use the <literal>RT*StrategyNumber</literal> constants.  It
+       returns whatever is passed to it.  The <literal>btree_gist</literal>
+       extension defines a second translation function,
+       <literal>gist_stratnum_btree</literal>, for operator classes that use
+       the <literal>BT*StrategyNumber</literal> constants.
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aac4..3a19dab15e0 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
+        used by the operator class (optional)</entry>
index 78e98d68b15..8686735f234 100644
@@ -21,6 +21,7 @@
 #include "common/pg_prng.h"
@@ -1055,3 +1056,16 @@ gistGetFakeLSN(Relation rel)
index 36b5a85cf31..0901543a60a 100644
@@ -146,6 +146,10 @@ gistvalidate(Oid opclassoid)
@@ -266,7 +270,8 @@ gistvalidate(Oid opclassoid)
@@ -338,6 +343,7 @@ gistadjustmembers(Oid opfamilyoid,
 
 /*
 
 # gin
index 73d9cf85826..7ea6e0fcff5 100644
@@ -12208,4 +12208,10 @@
 
+# GiST stratnum implementations
+{ oid => '8047', descr => 'GiST support',
+  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proargtypes => 'int2',
+  prosrc => 'gist_stratnum_identity' },
+
 ]
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 35fb72f302b..5f7bf6b8af6 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -819,3 +819,16 @@ SELECT pg_column_toast_chunk_id(a) IS NULL,
 
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
+-- test stratnum support functions
+SELECT gist_stratnum_identity(3::smallint);
+ gist_stratnum_identity 
+------------------------
+                      3
+(1 row)
+
+SELECT gist_stratnum_identity(18::smallint);
+ gist_stratnum_identity 
+------------------------
+                     18
+(1 row)
+
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index e570783453c..1e90d60af35 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -360,3 +360,7 @@ SELECT pg_column_toast_chunk_id(a) IS NULL,
   FROM test_chunk_id;
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
+
+-- test stratnum support functions
+SELECT gist_stratnum_identity(3::smallint);
+SELECT gist_stratnum_identity(18::smallint);
-- 
2.42.0

From 94c9d23e5e3171da448544a3f5363965c80873ef Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 24 Jan 2024 15:43:41 +0100
Subject: [PATCH v36 2/3] Add temporal PRIMARY KEY and UNIQUE constraints

Add WITHOUT OVERLAPS clause to PRIMARY KEY and UNIQUE constraints.
These are backed by GiST indexes instead of B-tree indexes, since they
are essentially exclusion constraints with = for the scalar parts of
the key and && for the temporal part.
---
 contrib/btree_gist/Makefile                   |    2 +-
 .../btree_gist/expected/without_overlaps.out  |   44 +
 contrib/btree_gist/meson.build                |    1 +
 contrib/btree_gist/sql/without_overlaps.sql   |   25 +
 doc/src/sgml/catalogs.sgml                    |   10 +
 doc/src/sgml/gist.sgml                        |   14 +-
 doc/src/sgml/ref/create_table.sgml            |   42 +-
 src/backend/access/gist/gistutil.c            |   29 +
 src/backend/catalog/heap.c                    |    1 +
 src/backend/catalog/index.c                   |    4 +
 src/backend/catalog/pg_constraint.c           |    2 +
 src/backend/commands/indexcmds.c              |  143 ++-
 src/backend/commands/tablecmds.c              |    6 +-
 src/backend/commands/trigger.c                |    1 +
 src/backend/commands/typecmds.c               |    2 +
 src/backend/executor/execIndexing.c           |    2 +-
 src/backend/optimizer/util/plancat.c          |    9 +-
 src/backend/parser/gram.y                     |   29 +-
 src/backend/parser/parse_utilcmd.c            |   28 +-
 src/backend/utils/adt/ruleutils.c             |    2 +
 src/backend/utils/cache/relcache.c            |   18 +-
 src/bin/pg_dump/pg_dump.c                     |   16 +-
 src/bin/pg_dump/pg_dump.h                     |    1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |   36 +
 src/bin/psql/describe.c                       |   12 +-
 src/include/access/gist.h                     |    3 +
 src/include/catalog/index.h                   |    1 +
 src/include/catalog/pg_constraint.h           |   10 +-
 src/include/commands/defrem.h                 |    6 +-
 src/include/nodes/parsenodes.h                |    2 +
 .../regress/expected/without_overlaps.out     | 1088 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/without_overlaps.sql     |  754 ++++++++++++
 33 files changed, 2293 insertions(+), 52 deletions(-)
 create mode 100644 contrib/btree_gist/expected/without_overlaps.out
 create mode 100644 contrib/btree_gist/sql/without_overlaps.sql
 create mode 100644 src/test/regress/expected/without_overlaps.out
 create mode 100644 src/test/regress/sql/without_overlaps.sql

index 9ab8548bc0e..7ac2df26c10 100644

index b654fae1b2f..1e56edaa70b 100644
@@ -2730,6 +2730,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>conperiod</structfield> <type>bool</type>
+      </para>
+      <para>
+       This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
+       (for primary keys and unique constraints).
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>conkey</structfield> <type>int2[]</type>
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index f789824c83b..dcf9433fa78 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -292,7 +292,8 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    The optional twelfth method <function>stratnum</function> is used to
    translate well-known <literal>RT*StrategyNumber</literal>s (from
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
-   used by the operator class.
+   used by the operator class.  This lets the core code look up operators for
+   temporal constraint indexes.
  </para>
 
  <variablelist>
@@ -1179,6 +1180,17 @@ my_sortsupport(PG_FUNCTION_ARGS)
        operator class has no matching strategy.
       </para>
 
+      <para>
+       This is used for temporal index constraints (i.e., <literal>PRIMARY
+       KEY</literal> and <literal>UNIQUE</literal>).  If the operator class
+       provides this function and it returns results for
+       <literal>RTEqualStrategyNumber</literal>, it can be used in the
+       non-<literal>WITHOUT OVERLAPS</literal> part(s) of an index constraint.
+       If it returns results for <literal>RTOverlapStrategyNumber</literal>,
+       the operator class can be used in the <literal>WITHOUT
+       OVERLAPS</literal> part of an index constraint.
+      </para>
+
       <para>
        The <acronym>SQL</acronym> declaration of the function must look like
        this:
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index f19306e7760..f2cda0c0e94 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -77,8 +77,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 
@@ -964,7 +964,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM

     <listitem>
@@ -978,6 +978,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       of these columns.
      </para>
 
+     <para>
+      If the <literal>WITHOUT OVERLAPS</literal> option is specified for the
+      last column, then that column is checked for overlaps instead of
+      equality.  In that case, the other columns of the constraint will allow
+      duplicates so long as the duplicates don't overlap in the
+      <literal>WITHOUT OVERLAPS</literal> column.  (This is sometimes called a
+      temporal key, if the column is a range of dates or timestamps, but
+      PostgreSQL allows ranges over any base type.)  In effect, such a
+      constraint is enforced with an <literal>EXCLUDE</literal> constraint
+      rather than a <literal>UNIQUE</literal> constraint.  So for example
+      <literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves like
+      <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
+      &amp;&amp;)</literal>.  The <literal>WITHOUT OVERLAPS</literal> column
+      must have a range or multirange type.  (Technically, any type is allowed
+      whose default GiST opclass includes an overlaps operator.  See the
+      <literal>stratnum</literal> support function under <xref
+      linkend="gist-extensibility"/> for details.)  The non-<literal>WITHOUT
+      OVERLAPS</literal> columns of the constraint can be any type that can be
+      compared for equality in a GiST index.  By default, only range types are
+      supported, but you can use other types by adding the <xref
+      linkend="btree-gist"/> extension (which is the expected way to use this
+      feature).
+     </para>
+
      <para>
       For the purpose of a unique constraint, null values are not
       considered equal, unless <literal>NULLS NOT DISTINCT</literal> is
@@ -1000,8 +1024,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
      <para>
       Adding a unique constraint will automatically create a unique btree
-      index on the column or group of columns used in the constraint.  The
-      created index has the same name as the unique constraint.
+      index on the column or group of columns used in the constraint.  But if
+      the constraint includes a <literal>WITHOUT OVERLAPS</literal> clause, it
+      will use a GiST index.  The created index has the same name as the
+      unique constraint.
      </para>
 
      <para>
 
    <varlistentry id="sql-createtable-parms-primary-key">
     <term><literal>PRIMARY KEY</literal> (column constraint)</term>
-    <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
+    <term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] )</literal>
     <optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
     <listitem>
      <para>
@@ -1054,8 +1080,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      <para>
       Adding a <literal>PRIMARY KEY</literal> constraint will automatically
       create a unique btree index on the column or group of columns used in
-      the constraint.  That index has the same name as the primary key
-      constraint.
+      the constraint, or GiST if <literal>WITHOUT OVERLAPS</literal> was
+      specified.
      </para>
 
      <para>
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 8686735f234..d2d0b36d4ea 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1069,3 +1069,32 @@ gist_stratnum_identity(PG_FUNCTION_ARGS)
 
 	PG_RETURN_UINT16(strat);
 }
+
+/*
index 00074c8a948..8ea7768e244 100644
@@ -2163,6 +2163,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
+							  false,	/* conperiod */
 
 	pfree(ccbin);
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index a819b4197ce..5c48e5728e9 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1874,6 +1874,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId)
  *		INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row
  *		INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies
  *			of index on table's columns
+ *		INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS: constraint uses WITHOUT OVERLAPS
  * allow_system_table_mods: allow table to be a system catalog
  * is_internal: index is constructed due to internal process
  */
@@ -1897,11 +1898,13 @@ index_constraint_create(Relation heapRelation,
 	bool		mark_as_primary;
 	bool		islocal;
 	bool		noinherit;
+	bool		is_without_overlaps;
 	int			inhcount;

 	/* constraint creation support doesn't work while bootstrapping */
 	Assert(!IsBootstrapProcessingMode());
@@ -1978,6 +1981,7 @@ index_constraint_create(Relation heapRelation,
 								   islocal,
 								   inhcount,
 								   noinherit,
+								   is_without_overlaps,
 								   is_internal);
 
 	/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 3baf9231ed0..9be050ccee8 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -75,6 +75,7 @@ CreateConstraintEntry(const char *constraintName,
 					  bool conIsLocal,
 					  int conInhCount,
 					  bool conNoInherit,
+					  bool conPeriod,
 					  bool is_internal)
 {
 	Relation	conDesc;
@@ -190,6 +191,7 @@ CreateConstraintEntry(const char *constraintName,
 	values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal);
 	values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
 	values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
+	values[Anum_pg_constraint_conperiod - 1] = BoolGetDatum(conPeriod);
 
 	if (conkeyArray)
index c5a56c75f69..47e139b3828 100644
 
@@ -87,6 +88,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
@@ -145,6 +147,7 @@ typedef struct ReindexErrorInfo
@@ -174,7 +177,8 @@ bool
@@ -249,8 +253,8 @@ CheckIndexCompatible(Oid oldId,
 
 	/* Get the soon-obsolete pg_index tuple. */
 	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -560,6 +564,7 @@ DefineIndex(Oid tableId,
 	bool		amcanorder;
 	bool		amissummarizing;
 	amoptions_function amoptions;
+	bool		exclusion;
 	bool		partitioned;
 	bool		safe_index;
 	Datum		reloptions;
@@ -680,6 +685,12 @@ DefineIndex(Oid tableId,

+	/*
+	 * It has exclusion constraint behavior if it's an EXCLUDE constraint or a
+	 * temporal PRIMARY KEY/UNIQUE constraint
+	 */
+	exclusion = stmt->excludeOpNames || stmt->iswithoutoverlaps;
+
 	/* Ensure that it makes sense to index this kind of relation */
 	switch (rel->rd_rel->relkind)
 	{
@@ -848,7 +859,7 @@ DefineIndex(Oid tableId,
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
 								 accessMethodId);
 
@@ -863,7 +874,7 @@ DefineIndex(Oid tableId,
@@ -916,8 +927,9 @@ DefineIndex(Oid tableId,
 
 	/*
@@ -935,7 +947,7 @@ DefineIndex(Oid tableId,
@@ -989,10 +1001,10 @@ DefineIndex(Oid tableId,
@@ -1031,12 +1043,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -1045,7 +1057,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1189,6 +1201,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+	if (stmt->iswithoutoverlaps)
+		constr_flags |= INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1855,6 +1869,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool iswithoutoverlaps,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1878,6 +1893,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;

@@ -2157,6 +2180,21 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
+			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
+											 &opid, &strat);
 
 		/*
 		 * Set up the per-column options (indoption field).  For now, this is
@@ -2387,6 +2425,83 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
 	return InvalidOid;
 }
 
+/*
+ * GetOperatorFromWellKnownStrategy
+ *
+ * opclass - the opclass to use
+ * atttype - the type to ask about
+ * opid - holds the operator we found
+ * strat - holds the input and output strategy number
+ *
+ * Finds an operator from a "well-known" strategy number.  This is used for
+ * temporal index constraints (and other temporal features) to look up
+ * equality and overlaps operators, since the strategy numbers for non-btree
+ * indexams need not follow any fixed scheme.  We ask an opclass support
+ * function to translate from the well-known number to the internal value.  If
+ * the function isn't defined or it gives no result, we return
+ * InvalidStrategy.
+ */
+void
+GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
+								 Oid *opid, StrategyNumber *strat)
+{
+	Oid			opfamily;
+	Oid			opcintype;
+	StrategyNumber instrat = *strat;
+
+	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+
+	*opid = InvalidOid;
+
+	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+	{
+		/*
+		 * Ask the opclass to translate to its internal stratnum
+		 *
+		 * For now we only need GiST support, but this could support other
+		 * indexams if we wanted.
+		 */
+		*strat = GistTranslateStratnum(opclass, instrat);
+		if (*strat == InvalidStrategy)
+		{
+			HeapTuple	tuple;
+
+			tuple = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for operator class %u", opclass);
+
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					instrat == RTEqualStrategyNumber ?
+					errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
+					errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+					errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
+							  instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
+
+			ReleaseSysCache(tuple);
+		}
+
+		*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
+	}
+
+	if (!OidIsValid(*opid))
+	{
+		HeapTuple	tuple;
+
+		tuple = SearchSysCache1(OPFAMILYOID, ObjectIdGetDatum(opfamily));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for operator family %u", opfamily);
+
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				instrat == RTEqualStrategyNumber ?
+				errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
+				errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+				errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
+						  NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
+	}
+}
+
 /*
  *	makeObjectName()
  *
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 721d24783b4..c39e8bc161d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10152,6 +10152,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
+									  false,	/* conPeriod */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10450,6 +10451,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conPeriod */
 									  false);
 
 			/*
@@ -10955,6 +10957,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conPeriod */
 								  true);
 
@@ -14124,7 +14127,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 170360edda8..29d30bfb6f7 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -834,6 +834,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 											  true, /* islocal */
 											  0,	/* inhcount */
 											  true, /* noinherit */
+											  false,	/* conperiod */
 											  isInternal);	/* is_internal */
 	}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 2a1e7133356..2a6550de907 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3621,6 +3621,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  true, /* is local */
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
+							  false,	/* conperiod */
 							  false);	/* is_internal */
 	if (constrAddr)
 		ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
@@ -3727,6 +3728,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  true, /* is local */
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
+							  false,	/* conperiod */
 							  false);	/* is_internal */
 
 	if (constrAddr)
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 9f05b3654c1..59acf67a36a 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -210,7 +210,7 @@ ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative)
 		 * If the indexes are to be used for speculative insertion, add extra
 		 * information required by unique index entries.
 		 */
-		if (speculative && ii->ii_Unique)
+		if (speculative && ii->ii_Unique && !indexDesc->rd_index->indisexclusion)
 			BuildSpeculativeIndexInfo(indexDesc, ii);
 
 		relationDescs[i] = indexDesc;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 9efdd844aac..eee69976ec9 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -817,7 +817,7 @@ infer_arbiter_indexes(PlannerInfo *root)
 		 */
 		if (indexOidFromConstraint == idxForm->indexrelid)
 		{
-			if (!idxForm->indisunique && onconflict->action == ONCONFLICT_UPDATE)
+			if (idxForm->indisexclusion && onconflict->action == ONCONFLICT_UPDATE)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints")));
@@ -842,6 +842,13 @@ infer_arbiter_indexes(PlannerInfo *root)
 		if (!idxForm->indisunique)
 			goto next;
 
+		/*
+		 * So-called unique constraints with WITHOUT OVERLAPS are really
+		 * exclusion constraints, so skip those too.
+		 */
+		if (idxForm->indisexclusion)
+			goto next;
+
 		/* Build BMS representation of plain (non expression) index attrs */
 		indexedAttrs = NULL;
 		for (natt = 0; natt < idxForm->indnkeyatts; natt++)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a043fd4c669..eafa290b88b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -532,7 +532,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				columnref in_expr having_clause func_table xmltable array_expr
 				OptWhereClause operator_def_arg
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
-%type <boolean> opt_ordinality
+%type <boolean> opt_ordinality opt_without_overlaps
 %type <list>	ExclusionConstraintList ExclusionConstraintElem
 %type <list>	func_arg_list func_arg_list_opt
 %type <node>	func_arg_expr
@@ -4193,7 +4193,7 @@ ConstraintElem:
 					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
-			| UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+			| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4202,11 +4202,12 @@ ConstraintElem:
 					n->location = @1;
 					n->nulls_not_distinct = !$2;
 					n->keys = $4;
-					n->including = $6;
-					n->options = $7;
+					n->without_overlaps = $5;
+					n->including = $7;
+					n->options = $8;
 					n->indexname = NULL;
-					n->indexspace = $8;
-					processCASbits($9, @9, "UNIQUE",
+					n->indexspace = $9;
+					processCASbits($10, @10, "UNIQUE",
 								   &n->deferrable, &n->initdeferred, NULL,
 								   NULL, yyscanner);
 					$$ = (Node *) n;
@@ -4227,7 +4228,7 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
-			| PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+			| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4235,11 +4236,12 @@ ConstraintElem:
 					n->contype = CONSTR_PRIMARY;
 					n->location = @1;
 					n->keys = $4;
-					n->including = $6;
-					n->options = $7;
+					n->without_overlaps = $5;
+					n->including = $7;
+					n->options = $8;
 					n->indexname = NULL;
-					n->indexspace = $8;
-					processCASbits($9, @9, "PRIMARY KEY",
+					n->indexspace = $9;
+					processCASbits($10, @10, "PRIMARY KEY",
 								   &n->deferrable, &n->initdeferred, NULL,
 								   NULL, yyscanner);
 					$$ = (Node *) n;
@@ -4361,6 +4363,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 			| /* EMPTY */							{  $$ = false; }
 		;
 
+opt_without_overlaps:
index d5c2b2ff0b0..53492c644a9 100644
@@ -1558,6 +1558,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
@@ -1607,7 +1608,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			nElems;
 				int			i;
 
@@ -2160,6 +2163,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 
@@ -2252,6 +2256,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 					 errmsg("index \"%s\" is not valid", index_name),
 					 parser_errposition(cxt->pstate, constraint->location)));
 
+		/*
+		 * Today we forbid non-unique indexes, but we could permit GiST
+		 * indexes whose last entry is a range type and use that to create a
+		 * WITHOUT OVERLAPS constraint (i.e. a temporal constraint).
+		 */
 		if (!index_form->indisunique)
 			ereport(ERROR,
 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -2540,6 +2549,23 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				notnullcmds = lappend(notnullcmds, notnullcmd);
 			}
 		}
+
+		if (constraint->without_overlaps)
+		{
+			/*
+			 * This enforces that there is at least one equality column
+			 * besides the WITHOUT OVERLAPS columns.  This is per SQL
+			 * standard.  XXX Do we need this?
+			 */
+			if (list_length(constraint->keys) < 2)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("constraint using WITHOUT OVERLAPS needs at least two columns"));
+
+			/* WITHOUT OVERLAPS requires a GiST index */
+			index->accessMethod = "gist";
+		}
+
 	}
 
 	/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 653685bffc5..50c0247c300 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2393,6 +2393,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 											 Anum_pg_constraint_conkey);
 
 				keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+				if (conForm->conperiod)
+					appendStringInfoString(&buf, " WITHOUT OVERLAPS");

index 66ed24e4012..5e7a1499e1e 100644
@@ -5581,11 +5581,14 @@ RelationGetIdentityKeyBitmap(Relation relation)
@@ -5649,7 +5652,10 @@ RelationGetExclusionInfo(Relation indexRelation,
 		int			nelem;
 
+			 !(conform->conperiod && (
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8b1888bd33..67127dbd9fe 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7326,6 +7326,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conperiod,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7407,10 +7408,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
+							 "c.conperiod ");
+							 "NULL AS conperiod ");
 
 	/*
 	 * The point of the messy-looking outer join is to find a constraint that
@@ -7478,6 +7486,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_conname = PQfnumber(res, "conname");
 	i_condeferrable = PQfnumber(res, "condeferrable");
 	i_condeferred = PQfnumber(res, "condeferred");
+	i_conperiod = PQfnumber(res, "conperiod");
 	i_contableoid = PQfnumber(res, "contableoid");
 	i_conoid = PQfnumber(res, "conoid");
 	i_condef = PQfnumber(res, "condef");
@@ -7585,6 +7594,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->conindex = indxinfo[j].dobj.dumpId;
 				constrinfo->condeferrable = *(PQgetvalue(res, j, i_condeferrable)) == 't';
 				constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
+				constrinfo->conperiod = *(PQgetvalue(res, j, i_conperiod)) == 't';
 				constrinfo->conislocal = true;
 				constrinfo->separate = true;
 
@@ -16954,6 +16964,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conperiod)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
 			if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
 				appendPQExpBufferStr(q, ") INCLUDE (");
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 4b2e5870a9c..b25b9f9304e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -474,6 +474,7 @@ typedef struct _constraintInfo
 	DumpId		conindex;		/* identifies associated index if any */
 	bool		condeferrable;	/* true if constraint is DEFERRABLE */
 	bool		condeferred;	/* true if constraint is INITIALLY DEFERRED */
+	bool		conperiod;		/* true if the constraint is WITHOUT OVERLAPS */
 	bool		conislocal;		/* true if constraint has local definition */
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index d3dd8784d64..beed8db4d10 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1004,6 +1004,42 @@ my %tests = (
 		},
 	},
 
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
index 7c9a1f234c6..90b2edb552f 100644
@@ -2383,6 +2383,10 @@ describeOneTableDetails(const char *schemaname,
+				appendPQExpBufferStr(&buf, ", con.conperiod");
+				appendPQExpBufferStr(&buf, ", false AS conperiod");
@@ -2404,8 +2408,12 @@ describeOneTableDetails(const char *schemaname,

 #include "access/itup.h"
 
index 7d434f8e653..2dea96f47c3 100644
@@ -93,6 +93,7 @@ extern Oid	index_create(Relation heapRelation,
 
index 7a8017f15b5..96c00624b15 100644
 
+	/*
+	bool		conperiod;
 
 	/*
 
 	/*
 
@@ -238,6 +245,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
+								  bool conPeriod,
 								  bool is_internal);
 
index 29c511e3196..0c53d67d3ee 100644
 
@@ -44,10 +45,13 @@ extern char *ChooseRelationName(const char *name1, const char *name2,
+											 Oid *opid, StrategyNumber *strat);
 
 /* commands/functioncmds.c */
 extern ObjectAddress CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 85a62b538e5..9dc4667d2bd 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2750,6 +2750,7 @@ typedef struct Constraint
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
 	List	   *keys;			/* String nodes naming referenced key
 								 * column(s); for UNIQUE/PK/NOT NULL */
+	bool		without_overlaps;	/* WITHOUT OVERLAPS specified */
 	List	   *including;		/* String nodes naming referenced nonkey
 								 * column(s); for UNIQUE/PK */
 	List	   *exclusions;		/* list of (IndexElem, operator name) pairs;
@@ -3373,6 +3374,7 @@ typedef struct IndexStmt
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
 	bool		primary;		/* is index a primary key? */
 	bool		isconstraint;	/* is it for a pkey/unique constraint? */
+	bool		iswithoutoverlaps;	/* is the constraint WITHOUT OVERLAPS? */
 	bool		deferrable;		/* is the constraint DEFERRABLE? */
 	bool		initdeferred;	/* is the constraint INITIALLY DEFERRED? */
 	bool		transformed;	/* true when transformIndexStmt is finished */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644
index 00000000000..94234758598
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,1088 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+SET datestyle TO ISO, YMD;
+--
+-- test input parser
+--
+-- PK with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng (
+	valid_at daterange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+ERROR:  constraint using WITHOUT OVERLAPS needs at least two columns
+-- PK with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng (
+	id INTEGER,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" named in key does not exist
+LINE 3:  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU...
+         ^
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  data type text has no default operator class for access method "gist"
+HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+-- PK with one column plus a range:
+CREATE TABLE temporal_rng (
+	-- Since we can't depend on having btree_gist here,
+	-- use an int4range instead of an int.
+	-- (The rangetypes regression test uses the same trick.)
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+              Table "public.temporal_rng"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+            pg_get_constraintdef             
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+                                pg_get_indexdef                                
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
+(1 row)
+
+-- PK from LIKE:
+CREATE TABLE temporal_rng2 (LIKE temporal_rng INCLUDING ALL);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng2_pkey" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+DROP TABLE temporal_rng2;
+-- no PK from INHERITS:
+CREATE TABLE temporal_rng2 () INHERITS (temporal_rng);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Inherits: temporal_rng
+
+DROP TABLE temporal_rng2;
+DROP TABLE temporal_rng;
+-- PK in inheriting table:
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+CREATE TABLE temporal_rng2 (
+  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) INHERITS (temporal_rng);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Inherits: temporal_rng
+
+DROP TABLE temporal_rng CASCADE;
+NOTICE:  drop cascades to table temporal_rng2
+-- Add PK to already inheriting table:
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+CREATE TABLE temporal_rng2 () INHERITS (temporal_rng);
+ALTER TABLE temporal_rng2
+  ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Inherits: temporal_rng
+
+DROP TABLE temporal_rng2;
+DROP TABLE temporal_rng;
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id1      | int4range |           | not null | 
+ id2      | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+               pg_get_constraintdef                
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at)
+(1 row)
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+-- PK with one column plus a multirange:
+CREATE TABLE temporal_mltrng (
+  id int4range,
+  valid_at datemultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng
+               Table "public.temporal_mltrng"
+  Column  |      Type      | Collation | Nullable | Default 
+----------+----------------+-----------+----------+---------
+ id       | int4range      |           | not null | 
+ valid_at | datemultirange |           | not null | 
+Indexes:
+    "temporal_mltrng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_mltrng_pk';
+            pg_get_constraintdef             
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_mltrng_pk';
+                                   pg_get_indexdef                                   
+-------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_mltrng_pk ON temporal_mltrng USING gist (id, valid_at)
+(1 row)
+
+-- PK with two columns plus a multirange:
+CREATE TABLE temporal_mltrng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at datemultirange,
+  CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng2
+              Table "public.temporal_mltrng2"
+  Column  |      Type      | Collation | Nullable | Default 
+----------+----------------+-----------+----------+---------
+ id1      | int4range      |           | not null | 
+ id2      | int4range      |           | not null | 
+ valid_at | datemultirange |           | not null | 
+Indexes:
+    "temporal_mltrng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_mltrng2_pk';
+               pg_get_constraintdef                
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_mltrng2_pk';
+                                       pg_get_indexdef                                       
+---------------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_mltrng2_pk ON temporal_mltrng2 USING gist (id1, id2, valid_at)
+(1 row)
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng3 (
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR:  constraint using WITHOUT OVERLAPS needs at least two columns
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng3 (
+	id INTEGER,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" named in key does not exist
+LINE 3:  CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV...
+         ^
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  data type text has no default operator class for access method "gist"
+HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+             Table "public.temporal_rng3"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           |          | 
+ valid_at | daterange |           |          | 
+Indexes:
+    "temporal_rng3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+          pg_get_constraintdef          
+----------------------------------------
+ UNIQUE (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+                                 pg_get_indexdef                                 
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+	id1 int4range,
+	id2 int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+             Table "public.temporal_rng3"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id1      | int4range |           |          | 
+ id2      | int4range |           |          | 
+ valid_at | daterange |           |          | 
+Indexes:
+    "temporal_rng3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+             pg_get_constraintdef             
+----------------------------------------------
+ UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_rng3;
+-- UNIQUE with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at daterange
+);
+ALTER TABLE temporal_rng
+	ADD CONSTRAINT temporal_rng_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY USING INDEX idx_temporal3_uq;
+ERROR:  "idx_temporal3_uq" is not a unique index
+LINE 2:  ADD CONSTRAINT temporal3_pk
+             ^
+DETAIL:  Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+ERROR:  "idx_temporal3_uq" is not a unique index
+LINE 2:  ADD CONSTRAINT temporal3_uq
+             ^
+DETAIL:  Cannot create a primary key or unique constraint using such an index.
+DROP TABLE temporal3;
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+NOTICE:  ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "idx_temporal3_uq" to "temporal3_uq"
+DROP TABLE temporal3;
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at daterange,
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at daterange,
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+--
+-- range PK: test with existing rows
+--
+ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
+-- okay:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+  ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_rng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
+ROLLBACK;
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_rng;
+--
+-- range PK: test inserts
+--
+-- okay:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+-- should fail:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
+INSERT INTO temporal_rng (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+ERROR:  null value in column "id" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains (null, [2018-01-01,2018-01-05)).
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
+ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains ([3,4), null).
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2018-01-02,2018-02-03)
+ [1,2) | [2018-03-03,2018-04-04)
+ [2,3) | [2018-01-01,2018-01-05)
+ [3,4) | [2018-01-01,)
+(4 rows)
+
+--
+-- range PK: test updates
+--
+-- update the scalar part
+UPDATE  temporal_rng
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the range part
+UPDATE  temporal_rng
+SET     valid_at = '[2020-01-01,2021-01-01)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_rng
+SET     id = '[21,22)',
+        valid_at = '[2018-01-02,2018-02-03)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+   id    |        valid_at         
+---------+-------------------------
+ [1,2)   | [2018-03-03,2018-04-04)
+ [2,3)   | [2018-01-01,2018-01-05)
+ [3,4)   | [2018-01-01,)
+ [21,22) | [2018-01-02,2018-02-03)
+(4 rows)
+
+-- should fail:
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = daterange('2018-03-05', '2018-05-05')
+WHERE   id = '[21,22)';
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-03-05,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+-- set the scalar part to NULL
+UPDATE  temporal_rng
+SET     id = NULL,
+        valid_at = daterange('2018-03-05', '2018-05-05')
+WHERE   id = '[21,22)';
+ERROR:  null value in column "id" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains (null, [2018-03-05,2018-05-05)).
+-- set the range part to NULL
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id = '[21,22)';
+ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
+DETAIL:  Failing row contains ([1,2), null).
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+   id    |        valid_at         
+---------+-------------------------
+ [1,2)   | [2018-03-03,2018-04-04)
+ [2,3)   | [2018-01-01,2018-01-05)
+ [3,4)   | [2018-01-01,)
+ [21,22) | [2018-01-02,2018-02-03)
+(4 rows)
+
+--
+-- range UQ: test with existing rows
+--
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at daterange
+);
+-- okay:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
+ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+  ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_rng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
+ROLLBACK;
+ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_rng3;
+--
+-- range UQ: test inserts
+--
+-- okay:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
+-- should fail:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2018-01-02,2018-02-03)
+ [1,2) | [2018-03-03,2018-04-04)
+ [2,3) | [2018-01-01,2018-01-05)
+ [3,4) | [2018-01-01,)
+ [3,4) | 
+       | [2018-01-01,2018-01-05)
+(6 rows)
+
+--
+-- range UQ: test updates
+--
+-- update the scalar part
+UPDATE  temporal_rng3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the range part
+UPDATE  temporal_rng3
+SET     valid_at = '[2020-01-01,2021-01-01)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_rng3
+SET     id = '[21,22)',
+        valid_at = '[2018-01-02,2018-02-03)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+-- set the scalar part to NULL
+UPDATE  temporal_rng3
+SET     id = NULL,
+        valid_at = daterange('2020-01-01', '2021-01-01')
+WHERE   id = '[21,22)';
+-- set the range part to NULL
+UPDATE  temporal_rng3
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2018-03-03,2018-04-04)
+ [1,2) | 
+ [2,3) | [2018-01-01,2018-01-05)
+ [3,4) | [2018-01-01,)
+ [3,4) | 
+       | [2018-01-01,2018-01-05)
+(6 rows)
+
+-- should fail:
+UPDATE  temporal_rng3
+SET     valid_at = daterange('2018-03-01', '2018-05-05')
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  conflicting key value violates exclusion constraint "temporal_rng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-03-01,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2018-03-03,2018-04-04)
+ [1,2) | 
+ [2,3) | [2018-01-01,2018-01-05)
+ [3,4) | [2018-01-01,)
+ [3,4) | 
+       | [2018-01-01,2018-01-05)
+(6 rows)
+
+DROP TABLE temporal_rng3;
+--
+-- multirange PK: test with existing rows
+--
+ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
+-- okay:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+  ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_mltrng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}) conflicts with key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}).
+ROLLBACK;
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_mltrng;
+--
+-- multirange PK: test inserts
+--
+-- okay:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+-- should fail:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}).
+INSERT INTO temporal_mltrng (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+ERROR:  null value in column "id" of relation "temporal_mltrng" violates not-null constraint
+DETAIL:  Failing row contains (null, {[2018-01-01,2018-01-05)}).
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', NULL);
+ERROR:  null value in column "valid_at" of relation "temporal_mltrng" violates not-null constraint
+DETAIL:  Failing row contains ([3,4), null).
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+  id   |         valid_at          
+-------+---------------------------
+ [1,2) | {[2018-01-02,2018-02-03)}
+ [1,2) | {[2018-03-03,2018-04-04)}
+ [2,3) | {[2018-01-01,2018-01-05)}
+ [3,4) | {[2018-01-01,)}
+(4 rows)
+
+--
+-- multirange PK: test updates
+--
+-- update the scalar part
+UPDATE  temporal_mltrng
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the multirange part
+UPDATE  temporal_mltrng
+SET     valid_at = '{[2020-01-01,2021-01-01)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_mltrng
+SET     id = '[21,22)',
+        valid_at = '{[2018-01-02,2018-02-03)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+   id    |         valid_at          
+---------+---------------------------
+ [1,2)   | {[2018-03-03,2018-04-04)}
+ [2,3)   | {[2018-01-01,2018-01-05)}
+ [3,4)   | {[2018-01-01,)}
+ [21,22) | {[2018-01-02,2018-02-03)}
+(4 rows)
+
+-- should fail:
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = datemultirange(daterange('2018-03-05', '2018-05-05'))
+WHERE   id = '[21,22)';
+ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng_pk"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-03-05,2018-05-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-03-03,2018-04-04)}).
+-- set the scalar part to NULL
+UPDATE  temporal_mltrng
+SET     id = NULL,
+        valid_at = datemultirange(daterange('2018-03-05', '2018-05-05'))
+WHERE   id = '[21,22)';
+ERROR:  null value in column "id" of relation "temporal_mltrng" violates not-null constraint
+DETAIL:  Failing row contains (null, {[2018-03-05,2018-05-05)}).
+-- set the multirange part to NULL
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id = '[21,22)';
+ERROR:  null value in column "valid_at" of relation "temporal_mltrng" violates not-null constraint
+DETAIL:  Failing row contains ([1,2), null).
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+   id    |         valid_at          
+---------+---------------------------
+ [1,2)   | {[2018-03-03,2018-04-04)}
+ [2,3)   | {[2018-01-01,2018-01-05)}
+ [3,4)   | {[2018-01-01,)}
+ [21,22) | {[2018-01-02,2018-02-03)}
+(4 rows)
+
+--
+-- multirange UQ: test with existing rows
+--
+CREATE TABLE temporal_mltrng3 (
+	id int4range,
+	valid_at datemultirange
+);
+-- okay:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
+ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_mltrng3 DROP CONSTRAINT temporal_mltrng3_uq;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+  ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_mltrng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}) conflicts with key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}).
+ROLLBACK;
+ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_mltrng3;
+--
+-- multirange UQ: test inserts
+--
+-- okay:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
+-- should fail:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}).
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+  id   |         valid_at          
+-------+---------------------------
+ [1,2) | {[2018-01-02,2018-02-03)}
+ [1,2) | {[2018-03-03,2018-04-04)}
+ [2,3) | {[2018-01-01,2018-01-05)}
+ [3,4) | {[2018-01-01,)}
+ [3,4) | 
+       | {[2018-01-01,2018-01-05)}
+(6 rows)
+
+--
+-- multirange UQ: test updates
+--
+-- update the scalar part
+UPDATE  temporal_mltrng3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the multirange part
+UPDATE  temporal_mltrng3
+SET     valid_at = '{[2020-01-01,2021-01-01)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_mltrng3
+SET     id = '[21,22)',
+        valid_at = '{[2018-01-02,2018-02-03)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+-- set the scalar part to NULL
+UPDATE  temporal_mltrng3
+SET     id = NULL,
+        valid_at = datemultirange(daterange('2020-01-01', '2021-01-01'))
+WHERE   id = '[21,22)';
+-- set the multirange part to NULL
+UPDATE  temporal_mltrng3
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+  id   |         valid_at          
+-------+---------------------------
+ [1,2) | {[2018-03-03,2018-04-04)}
+ [1,2) | 
+ [2,3) | {[2018-01-01,2018-01-05)}
+ [3,4) | {[2018-01-01,)}
+ [3,4) | 
+       | {[2018-01-01,2018-01-05)}
+(6 rows)
+
+-- should fail:
+UPDATE  temporal_mltrng3
+SET     valid_at = datemultirange(daterange('2018-03-01', '2018-05-05'))
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), {[2018-03-01,2018-05-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-03-03,2018-04-04)}).
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+  id   |         valid_at          
+-------+---------------------------
+ [1,2) | {[2018-03-03,2018-04-04)}
+ [1,2) | 
+ [2,3) | {[2018-01-01,2018-01-05)}
+ [3,4) | {[2018-01-01,)}
+ [3,4) | 
+       | {[2018-01-01,2018-01-05)}
+(6 rows)
+
+DROP TABLE temporal_mltrng3;
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_at daterange,
+  id2 int8range,
+  name TEXT,
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar')
+;
+DROP TABLE temporal3;
+--
+-- test changing the PK's dependencies
+--
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ERROR:  column "valid_at" is in a primary key
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+--
+-- test PARTITION BY for ranges
+--
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+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
+(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)
+
+DROP TABLE temporal_partitioned;
+-- temporal UNIQUE:
+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 tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+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
+(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)
+
+DROP TABLE temporal_partitioned;
+-- ALTER TABLE REPLICA IDENTITY
+-- (should fail)
+ALTER TABLE temporal_rng REPLICA IDENTITY USING INDEX temporal_rng_pk;
+ERROR:  cannot use non-unique index "temporal_rng_pk" as replica identity
+--
+-- ON CONFLICT
+--
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+ [1,2) | [2010-01-01,2020-01-01)
+ [2,3) | [2005-01-01,2006-01-01)
+(3 rows)
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+ [1,2) | [2010-01-01,2020-01-01)
+ [2,3) | [2005-01-01,2006-01-01)
+(3 rows)
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+-- with a UNIQUE constraint:
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+ [1,2) | [2010-01-01,2020-01-01)
+ [2,3) | [2005-01-01,2006-01-01)
+(3 rows)
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+ [1,2) | [2010-01-01,2020-01-01)
+ [2,3) | [2005-01-01,2006-01-01)
+(3 rows)
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+  id   |        valid_at         
+-------+-------------------------
+ [1,2) | [2000-01-01,2010-01-01)
+(1 row)
+
+DROP TABLE temporal3;
+RESET datestyle;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 2429ec2bbaa..ddc155c195f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -78,7 +78,7 @@ test: brin_bloom brin_multi
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
 # ----------
-test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role
+test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps
 
 # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
 test: rules psql psql_crosstab amutils stats_ext collate.linux.utf8 collate.windows.win1252
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 00000000000..36b3e6dc02a
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,754 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- We leave behind several tables to test pg_dump etc:
+-- temporal_rng, temporal_rng2,
+-- temporal_fk_rng2rng.
+
+SET datestyle TO ISO, YMD;
+
+--
+-- test input parser
+--
+
+-- PK with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng (
+	valid_at daterange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng (
+	id INTEGER,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a non-range column:
+
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with one column plus a range:
+
+CREATE TABLE temporal_rng (
+	-- Since we can't depend on having btree_gist here,
+	-- use an int4range instead of an int.
+	-- (The rangetypes regression test uses the same trick.)
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk';
+
+-- PK from LIKE:
+CREATE TABLE temporal_rng2 (LIKE temporal_rng INCLUDING ALL);
+\d temporal_rng2
+DROP TABLE temporal_rng2;
+
+-- no PK from INHERITS:
+CREATE TABLE temporal_rng2 () INHERITS (temporal_rng);
+\d temporal_rng2
+DROP TABLE temporal_rng2;
+DROP TABLE temporal_rng;
+
+-- PK in inheriting table:
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+CREATE TABLE temporal_rng2 (
+  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) INHERITS (temporal_rng);
+\d temporal_rng2
+DROP TABLE temporal_rng CASCADE;
+
+-- Add PK to already inheriting table:
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+CREATE TABLE temporal_rng2 () INHERITS (temporal_rng);
+ALTER TABLE temporal_rng2
+  ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal_rng2
+DROP TABLE temporal_rng2;
+DROP TABLE temporal_rng;
+
+-- PK with two columns plus a range:
+CREATE TABLE temporal_rng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+
+-- PK with one column plus a multirange:
+CREATE TABLE temporal_mltrng (
+  id int4range,
+  valid_at datemultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_mltrng_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_mltrng_pk';
+
+-- PK with two columns plus a multirange:
+CREATE TABLE temporal_mltrng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at datemultirange,
+  CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_mltrng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_mltrng2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_mltrng2_pk';
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng3 (
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng3 (
+	id INTEGER,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+-- UNIQUE with two columns plus a range:
+CREATE TABLE temporal_rng3 (
+	id1 int4range,
+	id2 int4range,
+	valid_at daterange,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
+DROP TABLE temporal_rng3;
+
+-- UNIQUE with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+DROP TABLE temporal_rng3;
+DROP TYPE textrange2;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_rng (
+	id int4range,
+	valid_at daterange
+);
+ALTER TABLE temporal_rng
+	ADD CONSTRAINT temporal_rng_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- PK with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING INDEX (not possible):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange
+);
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
+ALTER TABLE temporal3
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE USING INDEX idx_temporal3_uq;
+DROP TABLE temporal3;
+
+-- Add range column and the PK at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at daterange,
+	ADD CONSTRAINT temporal3_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+-- Add range column and UNIQUE constraint at the same time
+CREATE TABLE temporal3 (
+	id int4range
+);
+ALTER TABLE temporal3
+	ADD COLUMN valid_at daterange,
+	ADD CONSTRAINT temporal3_uq
+	UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DROP TABLE temporal3;
+
+--
+-- range PK: test with existing rows
+--
+
+ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
+
+-- okay:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+  ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_rng;
+
+--
+-- range PK: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+
+-- should fail:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+--
+-- range PK: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_rng
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the range part
+UPDATE  temporal_rng
+SET     valid_at = '[2020-01-01,2021-01-01)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_rng
+SET     id = '[21,22)',
+        valid_at = '[2018-01-02,2018-02-03)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = daterange('2018-03-05', '2018-05-05')
+WHERE   id = '[21,22)';
+-- set the scalar part to NULL
+UPDATE  temporal_rng
+SET     id = NULL,
+        valid_at = daterange('2018-03-05', '2018-05-05')
+WHERE   id = '[21,22)';
+-- set the range part to NULL
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id = '[21,22)';
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+--
+-- range UQ: test with existing rows
+--
+
+CREATE TABLE temporal_rng3 (
+	id int4range,
+	valid_at daterange
+);
+
+-- okay:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
+ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+  ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_rng3;
+
+--
+-- range UQ: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[2,3)', daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01', NULL));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
+
+-- should fail:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+
+--
+-- range UQ: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_rng3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the range part
+UPDATE  temporal_rng3
+SET     valid_at = '[2020-01-01,2021-01-01)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_rng3
+SET     id = '[21,22)',
+        valid_at = '[2018-01-02,2018-02-03)'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+-- set the scalar part to NULL
+UPDATE  temporal_rng3
+SET     id = NULL,
+        valid_at = daterange('2020-01-01', '2021-01-01')
+WHERE   id = '[21,22)';
+-- set the range part to NULL
+UPDATE  temporal_rng3
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_rng3
+SET     valid_at = daterange('2018-03-01', '2018-05-05')
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
+DROP TABLE temporal_rng3;
+
+--
+-- multirange PK: test with existing rows
+--
+
+ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
+
+-- okay:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+  ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_mltrng;
+
+--
+-- multirange PK: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+
+-- should fail:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', NULL);
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+
+--
+-- multirange PK: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_mltrng
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the multirange part
+UPDATE  temporal_mltrng
+SET     valid_at = '{[2020-01-01,2021-01-01)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_mltrng
+SET     id = '[21,22)',
+        valid_at = '{[2018-01-02,2018-02-03)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = datemultirange(daterange('2018-03-05', '2018-05-05'))
+WHERE   id = '[21,22)';
+-- set the scalar part to NULL
+UPDATE  temporal_mltrng
+SET     id = NULL,
+        valid_at = datemultirange(daterange('2018-03-05', '2018-05-05'))
+WHERE   id = '[21,22)';
+-- set the multirange part to NULL
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id = '[21,22)';
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+
+--
+-- multirange UQ: test with existing rows
+--
+
+CREATE TABLE temporal_mltrng3 (
+	id int4range,
+	valid_at datemultirange
+);
+
+-- okay:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
+ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_mltrng3 DROP CONSTRAINT temporal_mltrng3_uq;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+  ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_mltrng3;
+
+--
+-- multirange UQ: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
+
+-- should fail:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+
+--
+-- multirange UQ: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_mltrng3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the multirange part
+UPDATE  temporal_mltrng3
+SET     valid_at = '{[2020-01-01,2021-01-01)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_mltrng3
+SET     id = '[21,22)',
+        valid_at = '{[2018-01-02,2018-02-03)}'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+-- set the scalar part to NULL
+UPDATE  temporal_mltrng3
+SET     id = NULL,
+        valid_at = datemultirange(daterange('2020-01-01', '2021-01-01'))
+WHERE   id = '[21,22)';
+-- set the multirange part to NULL
+UPDATE  temporal_mltrng3
+SET     id = '[1,2)',
+        valid_at = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_mltrng3
+SET     valid_at = datemultirange(daterange('2018-03-01', '2018-05-05'))
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
+DROP TABLE temporal_mltrng3;
+
+--
+-- test a range with both a PK and a UNIQUE constraint
+--
+
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_at daterange,
+  id2 int8range,
+  name TEXT,
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO temporal3 (id, valid_at, id2, name)
+  VALUES
+  ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar')
+;
+DROP TABLE temporal3;
+
+--
+-- test changing the PK's dependencies
+--
+
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
+ALTER TABLE temporal3 DROP COLUMN valid_thru;
+DROP TABLE temporal3;
+
+--
+-- test PARTITION BY for ranges
+--
+
+-- temporal PRIMARY KEY:
+CREATE TABLE temporal_partitioned (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+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;
+DROP TABLE temporal_partitioned;
+
+-- temporal UNIQUE:
+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 tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)');
+CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)');
+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;
+DROP TABLE temporal_partitioned;
+
+-- ALTER TABLE REPLICA IDENTITY
+-- (should fail)
+ALTER TABLE temporal_rng REPLICA IDENTITY USING INDEX temporal_rng_pk;
+
+--
+-- ON CONFLICT
+--
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING;
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+TRUNCATE temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal_rng_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_rng ORDER BY id, valid_at;
+
+-- with a UNIQUE constraint:
+
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT DO NOTHING;
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO NOTHING;
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO NOTHING;
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+
+TRUNCATE temporal3;
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2000-01-01', '2010-01-01'));
+-- with a conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[1,2)', daterange('2010-01-01', '2020-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal3 (id, valid_at) VALUES ('[2,3)', daterange('2005-01-01', '2006-01-01')) ON CONFLICT ON CONSTRAINT temporal3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal3 ORDER BY id, valid_at;
+
+DROP TABLE temporal3;
+
+RESET datestyle;
-- 
2.42.0

From 2f32839278b255a8a71dacfa510c78fa21c994d9 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Thu, 18 Jul 2024 09:52:56 -0700
Subject: [PATCH v36 3/3] WIP: Use nonempty_overlaps to forbid empty
 ranges/multiranges

Tests are failing. We get the "range cannot be empty" error even when
inserting a non-empty range, after a previous failure from an empty
range. `SELECT *` says the table has no rows. That is concerning. Is
the ereport causing the index to get corrupted?
---
 src/backend/commands/indexcmds.c              |  8 ++--
 src/backend/parser/parse_utilcmd.c            |  3 +-
 src/backend/utils/adt/multirangetypes.c       | 48 +++++++++++++++++++
 src/backend/utils/adt/rangetypes.c            | 23 +++++++++
 src/backend/utils/adt/rangetypes_gist.c       |  8 ++++
 src/include/access/stratnum.h                 |  1 +
 src/include/catalog/pg_amop.dat               |  6 +++
 src/include/catalog/pg_operator.dat           | 11 +++++
 src/include/catalog/pg_proc.dat               | 11 +++++
 src/include/utils/multirangetypes.h           |  6 +++
 src/include/utils/rangetypes.h                |  3 ++
 src/test/regress/expected/opr_sanity.out      | 13 +++--
 .../regress/expected/without_overlaps.out     | 26 ++++++++++
 src/test/regress/sql/without_overlaps.sql     | 18 +++++++
 14 files changed, 175 insertions(+), 10 deletions(-)

diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 47e139b3828..a73456a03b5 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2186,7 +2186,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 			Oid			opid;
 
 			if (attn == nkeycols - 1)
-				strat = RTOverlapStrategyNumber;
+				strat = RTNonemptyOverlapStrategyNumber;
 			else
 				strat = RTEqualStrategyNumber;
 			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
@@ -2449,7 +2449,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 	Oid			opcintype;
 	StrategyNumber instrat = *strat;
 
-	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+	Assert(instrat == RTEqualStrategyNumber || instrat == RTNonemptyOverlapStrategyNumber);
 
 	*opid = InvalidOid;
 
@@ -2474,7 +2474,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 					errcode(ERRCODE_UNDEFINED_OBJECT),
 					instrat == RTEqualStrategyNumber ?
 					errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-					errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+					errmsg("could not identify a nonempty overlaps operator for type %s", format_type_be(atttype)),
 					errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
 							  instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
 
@@ -2496,7 +2496,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 				errcode(ERRCODE_UNDEFINED_OBJECT),
 				instrat == RTEqualStrategyNumber ?
 				errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-				errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+				errmsg("could not identify a nonempty overlaps operator for type %s", format_type_be(atttype)),
 				errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
 						  NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
 	}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 53492c644a9..b6a84a7b43b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2397,7 +2397,8 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	 * For UNIQUE and PRIMARY KEY, we just have a list of column names.
 	 *
 	 * Make sure referenced keys exist.  If we are making a PRIMARY KEY index,
-	 * also make sure they are NOT NULL.
+	 * also make sure they are NOT NULL.  For WITHOUT OVERLAPS constraints,
+	 * we make sure the last part is a range or multirange.
 	 */
 	else
 	{
diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index 558c6c18c34..942f6d26de3 100644
--- a/src/backend/utils/adt/multirangetypes.c
+++ b/src/backend/utils/adt/multirangetypes.c
@@ -1968,6 +1968,30 @@ multirange_overlaps_multirange(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(multirange_overlaps_multirange_internal(typcache->rngtype, mr1, mr2));
 }
 
+Datum
+range_nonempty_overlaps_multirange(PG_FUNCTION_ARGS)
+{
+	RangeType  *r = PG_GETARG_RANGE_P(0);
+	MultirangeType *mr = PG_GETARG_MULTIRANGE_P(1);
+	TypeCacheEntry *typcache;
+
+	typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr));
+
+	PG_RETURN_BOOL(range_nonempty_overlaps_multirange_internal(typcache->rngtype, r, mr));
+}
+
+Datum
+multirange_nonempty_overlaps_multirange(PG_FUNCTION_ARGS)
+{
+	MultirangeType *mr1 = PG_GETARG_MULTIRANGE_P(0);
+	MultirangeType *mr2 = PG_GETARG_MULTIRANGE_P(1);
+	TypeCacheEntry *typcache;
+
+	typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr1));
+
+	PG_RETURN_BOOL(multirange_nonempty_overlaps_multirange_internal(typcache->rngtype, mr1, mr2));
+}
+
 /*
  * Comparison function for checking if any range of multirange overlaps given
  * key range using binary search.
@@ -2068,6 +2092,30 @@ multirange_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
 	return false;
 }
 
+bool
+range_nonempty_overlaps_multirange_internal(TypeCacheEntry *typcache,
+												 const RangeType *r,
+												 const MultirangeType *mr)
+{
+	if (RangeIsEmpty(r))
+		ereport(ERROR, (errmsg("range cannot be empty")));
+	if (MultirangeIsEmpty(mr))
+		ereport(ERROR, (errmsg("multirange cannot be empty")));
+
+	return range_overlaps_multirange_internal(typcache, r, mr);
+}
+
+bool
+multirange_nonempty_overlaps_multirange_internal(TypeCacheEntry *typcache,
+												 const MultirangeType *mr1,
+												 const MultirangeType *mr2)
+{
+	if (MultirangeIsEmpty(mr1) || MultirangeIsEmpty(mr2))
+		ereport(ERROR, (errmsg("multirange cannot be empty")));
+
+	return multirange_overlaps_multirange_internal(typcache, mr1, mr2);
+}
+
 /* does not extend to right of? */
 bool
 range_overleft_multirange_internal(TypeCacheEntry *rangetyp,
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 2d94a6b8774..6dad038037e 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -869,6 +869,16 @@ range_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1, const Ran
 	return false;
 }
 
+/* overlaps and neither empty? (internal version) */
+bool
+range_nonempty_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2)
+{
+	if (RangeIsEmpty(r1) || RangeIsEmpty(r2))
+		ereport(ERROR, (errmsg("range cannot be empty")));
+
+	return range_overlaps_internal(typcache, r1, r2);
+}
+
 /* overlaps? */
 Datum
 range_overlaps(PG_FUNCTION_ARGS)
@@ -882,6 +892,19 @@ range_overlaps(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(range_overlaps_internal(typcache, r1, r2));
 }
 
+/* overlaps and neither empty? */
+Datum
+range_nonempty_overlaps(PG_FUNCTION_ARGS)
+{
+	RangeType  *r1 = PG_GETARG_RANGE_P(0);
+	RangeType  *r2 = PG_GETARG_RANGE_P(1);
+	TypeCacheEntry *typcache;
+
+	typcache = range_get_typcache(fcinfo, RangeTypeGetOid(r1));
+
+	PG_RETURN_BOOL(range_nonempty_overlaps_internal(typcache, r1, r2));
+}
+
 /* does not extend to right of? (internal version) */
 bool
 range_overleft_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2)
diff --git a/src/backend/utils/adt/rangetypes_gist.c b/src/backend/utils/adt/rangetypes_gist.c
index cb28e9859ab..39de28cd163 100644
--- a/src/backend/utils/adt/rangetypes_gist.c
+++ b/src/backend/utils/adt/rangetypes_gist.c
@@ -929,6 +929,8 @@ range_gist_consistent_int_range(TypeCacheEntry *typcache,
 			return (!range_after_internal(typcache, key, query));
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_internal(typcache, key, query);
+		case RANGESTRAT_NONEMPTY_OVERLAPS:
+			return range_nonempty_overlaps_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			if (RangeIsEmpty(key) || RangeIsEmpty(query))
 				return false;
@@ -991,6 +993,8 @@ range_gist_consistent_int_multirange(TypeCacheEntry *typcache,
 			return (!range_after_multirange_internal(typcache, key, query));
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_multirange_internal(typcache, key, query);
+		case RANGESTRAT_NONEMPTY_OVERLAPS:
+			return range_nonempty_overlaps_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			if (RangeIsEmpty(key) || MultirangeIsEmpty(query))
 				return false;
@@ -1068,6 +1072,8 @@ range_gist_consistent_leaf_range(TypeCacheEntry *typcache,
 			return range_overleft_internal(typcache, key, query);
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_internal(typcache, key, query);
+		case RANGESTRAT_NONEMPTY_OVERLAPS:
+			return range_nonempty_overlaps_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			return range_overright_internal(typcache, key, query);
 		case RANGESTRAT_AFTER:
@@ -1103,6 +1109,8 @@ range_gist_consistent_leaf_multirange(TypeCacheEntry *typcache,
 			return range_overleft_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERLAPS:
 			return range_overlaps_multirange_internal(typcache, key, query);
+		case RANGESTRAT_NONEMPTY_OVERLAPS:
+			return range_nonempty_overlaps_multirange_internal(typcache, key, query);
 		case RANGESTRAT_OVERRIGHT:
 			return range_overright_multirange_internal(typcache, key, query);
 		case RANGESTRAT_AFTER:
diff --git a/src/include/access/stratnum.h b/src/include/access/stratnum.h
index 8a47d3c9ec8..58e8a40c0f0 100644
--- a/src/include/access/stratnum.h
+++ b/src/include/access/stratnum.h
@@ -78,6 +78,7 @@ typedef uint16 StrategyNumber;
 #define RTPrefixStrategyNumber			28	/* for text ^@ */
 #define RTOldBelowStrategyNumber		29	/* for old spelling of <<| */
 #define RTOldAboveStrategyNumber		30	/* for old spelling of |>> */
+#define RTNonemptyOverlapStrategyNumber	31	/* for &&& */
 
 #define RTMaxStrategyNumber				30
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index d8a05214b11..b8434915fdd 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1361,6 +1361,9 @@
 { amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anymultirange', amopstrategy => '3',
   amopopr => '&&(anyrange,anymultirange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
+  amoprighttype => 'anyrange', amopstrategy => '31',
+  amopopr => '&&&(anyrange,anyrange)', amopmethod => 'gist' },
 { amopfamily => 'gist/range_ops', amoplefttype => 'anyrange',
   amoprighttype => 'anyrange', amopstrategy => '4',
   amopopr => '&>(anyrange,anyrange)', amopmethod => 'gist' },
@@ -1417,6 +1420,9 @@
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
   amoprighttype => 'anyrange', amopstrategy => '3',
   amopopr => '&&(anymultirange,anyrange)', amopmethod => 'gist' },
+{ amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
+  amoprighttype => 'anymultirange', amopstrategy => '31',
+  amopopr => '&&&(anymultirange,anymultirange)', amopmethod => 'gist' },
 { amopfamily => 'gist/multirange_ops', amoplefttype => 'anymultirange',
   amoprighttype => 'anymultirange', amopstrategy => '4',
   amopopr => '&>(anymultirange,anymultirange)', amopmethod => 'gist' },
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 0e7511dde1c..d64a5a2c9e7 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3095,6 +3095,11 @@
   oprresult => 'bool', oprcom => '&&(anyrange,anyrange)',
   oprcode => 'range_overlaps', oprrest => 'rangesel',
   oprjoin => 'areajoinsel' },
+{ oid => '4552', oid_symbol => 'OID_RANGE_NONEMPTY_OVERLAP_OP', descr => 'nonempty overlaps',
+  oprname => '&&&', oprleft => 'anyrange', oprright => 'anyrange',
+  oprresult => 'bool', oprcom => '&&&(anyrange,anyrange)',
+  oprcode => 'range_nonempty_overlaps', oprrest => 'rangesel',
+  oprjoin => 'areajoinsel' },
 { oid => '3889', oid_symbol => 'OID_RANGE_CONTAINS_ELEM_OP',
   descr => 'contains',
   oprname => '@>', oprleft => 'anyrange', oprright => 'anyelement',
@@ -3314,6 +3319,12 @@
   oprresult => 'bool', oprcom => '&&(anymultirange,anymultirange)',
   oprcode => 'multirange_overlaps_multirange', oprrest => 'multirangesel',
   oprjoin => 'areajoinsel' },
+{ oid => '4553', oid_symbol => 'OID_MULTIRANGE_NONEMPTY_OVERLAPS_MULTIRANGE_OP',
+  descr => 'nonempty overlaps',
+  oprname => '&&&', oprleft => 'anymultirange', oprright => 'anymultirange',
+  oprresult => 'bool', oprcom => '&&&(anymultirange,anymultirange)',
+  oprcode => 'multirange_nonempty_overlaps_multirange', oprrest => 'multirangesel',
+  oprjoin => 'areajoinsel' },
 { oid => '2869', oid_symbol => 'OID_MULTIRANGE_CONTAINS_ELEM_OP',
   descr => 'contains',
   oprname => '@>', oprleft => 'anymultirange', oprright => 'anyelement',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7ea6e0fcff5..dcdcde81a07 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10552,6 +10552,9 @@
 { oid => '3857',
   proname => 'range_overlaps', prorettype => 'bool',
   proargtypes => 'anyrange anyrange', prosrc => 'range_overlaps' },
+{ oid => '4551',
+  proname => 'range_nonempty_overlaps', prorettype => 'bool',
+  proargtypes => 'anyrange anyrange', prosrc => 'range_nonempty_overlaps' },
 { oid => '3858',
   proname => 'range_contains_elem', prosupport => 'range_contains_elem_support',
   prorettype => 'bool', proargtypes => 'anyrange anyelement',
@@ -10795,6 +10798,14 @@
   proname => 'multirange_overlaps_multirange', prorettype => 'bool',
   proargtypes => 'anymultirange anymultirange',
   prosrc => 'multirange_overlaps_multirange' },
+{ oid => '4555',
+  proname => 'range_nonempty_overlaps_multirange', prorettype => 'bool',
+  proargtypes => 'anyrange anymultirange',
+  prosrc => 'range_nonempty_overlaps_multirange' },
+{ oid => '4554',
+  proname => 'multirange_nonempty_overlaps_multirange', prorettype => 'bool',
+  proargtypes => 'anymultirange anymultirange',
+  prosrc => 'multirange_nonempty_overlaps_multirange' },
 { oid => '4249',
   proname => 'multirange_contains_elem', prorettype => 'bool',
   proargtypes => 'anymultirange anyelement',
diff --git a/src/include/utils/multirangetypes.h b/src/include/utils/multirangetypes.h
index 845254395b6..23371108a46 100644
--- a/src/include/utils/multirangetypes.h
+++ b/src/include/utils/multirangetypes.h
@@ -92,9 +92,15 @@ extern bool multirange_contains_multirange_internal(TypeCacheEntry *rangetyp,
 extern bool range_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
 											   const RangeType *r,
 											   const MultirangeType *mr);
+extern bool range_nonempty_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
+											   const RangeType *r,
+											   const MultirangeType *mr);
 extern bool multirange_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
 													const MultirangeType *mr1,
 													const MultirangeType *mr2);
+extern bool multirange_nonempty_overlaps_multirange_internal(TypeCacheEntry *rangetyp,
+															 const MultirangeType *mr1,
+															 const MultirangeType *mr2);
 extern bool range_overleft_multirange_internal(TypeCacheEntry *rangetyp,
 											   const RangeType *r,
 											   const MultirangeType *mr);
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 2b574873cef..a9ac10e131d 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -97,6 +97,7 @@ RangeTypePGetDatum(const RangeType *X)
 #define RANGESTRAT_BEFORE				RTLeftStrategyNumber
 #define RANGESTRAT_OVERLEFT				RTOverLeftStrategyNumber
 #define RANGESTRAT_OVERLAPS				RTOverlapStrategyNumber
+#define RANGESTRAT_NONEMPTY_OVERLAPS	RTNonemptyOverlapStrategyNumber
 #define RANGESTRAT_OVERRIGHT			RTOverRightStrategyNumber
 #define RANGESTRAT_AFTER				RTRightStrategyNumber
 #define RANGESTRAT_ADJACENT				RTSameStrategyNumber
@@ -128,6 +129,8 @@ extern bool range_adjacent_internal(TypeCacheEntry *typcache, const RangeType *r
 									const RangeType *r2);
 extern bool range_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1,
 									const RangeType *r2);
+extern bool range_nonempty_overlaps_internal(TypeCacheEntry *typcache, const RangeType *r1,
+									const RangeType *r2);
 extern bool range_overleft_internal(TypeCacheEntry *typcache, const RangeType *r1,
 									const RangeType *r2);
 extern bool range_overright_internal(TypeCacheEntry *typcache, const RangeType *r1,
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 9d047b21b88..d8059a72702 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -518,9 +518,10 @@ SELECT p1.oid, p1.proname
 FROM pg_proc as p1 LEFT JOIN pg_description as d
      ON p1.tableoid = d.classoid and p1.oid = d.objoid and d.objsubid = 0
 WHERE d.classoid IS NULL AND p1.oid <= 9999;
- oid | proname 
------+---------
-(0 rows)
+ oid  |              proname               
+------+------------------------------------
+ 4555 | range_nonempty_overlaps_multirange
+(1 row)
 
 -- List of built-in leakproof functions
 --
@@ -1149,6 +1150,7 @@ ORDER BY 1, 2;
  #    | #
  &    | &
  &&   | &&
+ &&&  | &&&
  *    | *
  *<   | *>
  *<=  | *>=
@@ -1175,7 +1177,7 @@ ORDER BY 1, 2;
  ~<=~ | ~>=~
  ~<~  | ~>~
  ~=   | ~=
-(29 rows)
+(30 rows)
 
 -- Likewise for negator pairs.
 SELECT DISTINCT o1.oprname AS op1, o2.oprname AS op2
@@ -2010,6 +2012,7 @@ ORDER BY 1, 2, 3;
         783 |           28 | <@
         783 |           29 | <^
         783 |           30 | >^
+        783 |           31 | &&&
         783 |           48 | <@
         783 |           68 | <@
        2742 |            1 | &&
@@ -2090,7 +2093,7 @@ ORDER BY 1, 2, 3;
        4000 |           28 | ^@
        4000 |           29 | <^
        4000 |           30 | >^
-(124 rows)
+(125 rows)
 
 -- Check that all opclass search operators have selectivity estimators.
 -- This is not absolutely required, but it seems a reasonable thing
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 94234758598..04038a83fba 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -379,6 +379,11 @@ DROP TABLE temporal3;
 -- range PK: test with existing rows
 --
 ALTER TABLE temporal_rng DROP CONSTRAINT temporal_rng_pk;
+-- no empties allowed, even the first row:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', 'empty');
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  range cannot be empty
+DELETE FROM temporal_rng;
 -- okay:
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
@@ -398,6 +403,14 @@ DELETE FROM temporal_rng;
 --
 -- range PK: test inserts
 --
+-- should fail:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', 'empty');
+ERROR:  range cannot be empty
+SELECT * FROM temporal_rng;
+ id | valid_at 
+----+----------
+(0 rows)
+
 -- okay:
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
@@ -592,6 +605,11 @@ DROP TABLE temporal_rng3;
 -- multirange PK: test with existing rows
 --
 ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
+-- no empties allowed, even the first row:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', '{}');
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  range cannot be empty
+DELETE FROM temporal_mltrng;
 -- okay:
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
@@ -611,6 +629,14 @@ DELETE FROM temporal_mltrng;
 --
 -- multirange PK: test inserts
 --
+-- should fail:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', '{}');
+ERROR:  multirange cannot be empty
+SELECT * FROM temporal_mltrng;
+ id | valid_at 
+----+----------
+(0 rows)
+
 -- okay:
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 36b3e6dc02a..cf4a22345f9 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -251,6 +251,11 @@ DROP TABLE temporal3;

+-- no empties allowed, even the first row:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', 'empty');
+ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_rng;
+
 -- okay:
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
@@ -271,6 +276,10 @@ DELETE FROM temporal_rng;
 -- range PK: test inserts
 --
 
+-- should fail:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', 'empty');
+SELECT * FROM temporal_rng;
+
 -- okay:
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-03'));
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-03-03', '2018-04-04'));
@@ -408,6 +417,11 @@ DROP TABLE temporal_rng3;
 
 ALTER TABLE temporal_mltrng DROP CONSTRAINT temporal_mltrng_pk;
 
+-- no empties allowed, even the first row:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', '{}');
+ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_mltrng;
+
 -- okay:
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
@@ -428,6 +442,10 @@ DELETE FROM temporal_mltrng;
 -- multirange PK: test inserts
 --
 
+-- should fail:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', '{}');
+SELECT * FROM temporal_mltrng;
+
 -- okay:
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04')));
-- 
2.42.0

---

From: Paul Jungwirth <[email-masked]>
Date: Tue, 23 Jul 2024 09:08:05 -0700

> regression=# select gist_page_items(get_raw_page('temporal_rng_pk', 0), 'temporal_rng_pk');

I realized this isn't index corruption, just MVCC. The exclusion constraint is checked after we
update the index, which is why the row gets left behind. But it doesn't cause any wrong answers, and
if you vacuum the table the row goes away.

This also explains my confusion here:

> Except when I test it, it still works!

The first row still does a comparison because when we check the exclusion constraint, there is a
comparison between the query and the key we just inserted. (When I say "query" I don't mean a SQL
query, but the value used to search the index that is compared against its keys.)

So I'm glad I didn't stumble on a GiST bug, but I think it means ereporting from an exclusion operator
is not a workable approach. Failures leave behind invalid tuples, and future (valid) tuples can fail if
we compare to those invalid tuples. Since MVCC visibility is stored in the heap, not in the index, it's
not really accessible to us here. So far I don't have any ideas to rescue this idea, even though I like
it a lot. So I will go back to the executor idea we discussed at pgconf.dev.

One tempting alternative though is to let exclusion constraints do the not-empty check, instead of
putting it in the executor. It would be an extra check we do only when the constraint has
pg_constraint.conperiod. Then we don't need to add & maintain pg_class.relwithoutoverlaps, and we don't
need a relcache change, and we don't need so much extra code to check existing rows when you add the
constraint. It doesn't use the existing available exclusion constraint functionality, but if we're
willing to extend the executor to know about WITHOUT OVERLAPS, I guess we could teach exclusion
constraints about it instead. Doing the check there does seem to have better locality with the feature.
So I think I will try that out as well.

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 25 Jul 2024 08:52:44 -0700

> So I think I will try that out as well.

Here is a patch moving the not-empty check into check_exclusion_or_unique_constraint. That is a more 
logical place for it than ExecConstraints, since WITHOUT OVERLAPS is part of the index constraint 
(not a CHECK constraint). At that point we've already looked up all the information we need. So 
there is no extra cost for non-temporal tables, and no need to change pg_class or add to the 
relcache. Also putting it there means we don't need any extra code to enforce non-empties when we 
build the index or do anything else with it.

I think this is the nicest solution we can expect. It is even cleaner than the &&& ideas. So 
hopefully this gets us back to where we were when we decided to commit PKs & FKs to v17.

As before, I've left the nonempty check as a separate patch to make reviewing easier, but when 
committing I would squash it with the PK patch.

Rebased to 05faf06e9c.

Yours,

From 482bcce7ac6bef27a7771c785c6516d5c267380a Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 19 Jan 2024 15:41:44 +0100
Subject: [PATCH v37 1/8] Add stratnum GiST support function

 

 
 
 PG_MODULE_MAGIC;

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
 
 /*
 
 # gin
index d14a94b9873..cee40c1e4db 100644

From 8ccb3fb82b17b79a788d43690bc3765cf2fd4454 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 24 Jan 2024 15:43:41 +0100
Subject: [PATCH v37 2/8] Add temporal PRIMARY KEY and UNIQUE constraints

 

+     <row>
 
  <variablelist>
 
+      <para>

 
     <listitem>
 
+     <para>
 
      <para>
 
      <para>

      <para>
 
 	PG_RETURN_UINT16(strat);
 }
+
+/*
index 01b43cc6a84..78e59384d1c 100644
 
 	pfree(ccbin);

 
 	/*
 
 	if (conkeyArray)

 
 
+	/*

 	/*
 
-						if (stmt->unique)

 	indexRelationId =

 
 		/*
 
+/*

 			/*

 
 
 	if (constrAddr)

+		/*
 
+opt_without_overlaps:

 
+		/*
 
 	/*

 

 
 
 	/*

 

 #include "access/itup.h"

 
+	/*
 
 	/*
 
 	/*

 

From 5da9184fa82adfcb1ee43b44aa9a89a46dae1d64 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 1 Jun 2024 15:24:56 -0700
Subject: [PATCH v37 3/8] Forbid empty ranges/multiranges in WITHOUT OVERLAPS
 columns

Because 'empty' && 'empty' is false, the temporal PK/UQ constraint allows
duplicates, which is confusing to users and breaks internal
expectations. For instance when GROUP BY checks functional dependencies
on the PK, it allows selecting other columns from the table, but in the
presence of duplicate keys you could get the value from any of their
rows. So we need to forbid empties.

This all means we can only support ranges and multiranges for temporal
PK/UQs. So I added a check and updated the docs and tests.
---
 doc/src/sgml/gist.sgml                        |  3 -
 doc/src/sgml/ref/create_table.sgml            | 15 ++--
 src/backend/catalog/index.c                   | 10 ++-
 src/backend/commands/indexcmds.c              |  5 +-
 src/backend/executor/execIndexing.c           | 60 ++++++++++++++
 src/backend/nodes/makefuncs.c                 |  4 +-
 src/backend/parser/parse_utilcmd.c            | 47 ++++++++++-
 src/include/executor/executor.h               |  2 +
 src/include/nodes/execnodes.h                 |  1 +
 src/include/nodes/makefuncs.h                 |  2 +-
 .../regress/expected/without_overlaps.out     | 80 ++++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 56 +++++++++++++
 12 files changed, 260 insertions(+), 25 deletions(-)

diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index dcf9433fa78..638d912dc2d 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -1186,9 +1186,6 @@ my_sortsupport(PG_FUNCTION_ARGS)
        provides this function and it returns results for
        <literal>RTEqualStrategyNumber</literal>, it can be used in the
        non-<literal>WITHOUT OVERLAPS</literal> part(s) of an index constraint.
-       If it returns results for <literal>RTOverlapStrategyNumber</literal>,
-       the operator class can be used in the <literal>WITHOUT
-       OVERLAPS</literal> part of an index constraint.
       </para>
 
       <para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index f2cda0c0e94..9844d23be40 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -991,15 +991,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       <literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves like
       <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
       &amp;&amp;)</literal>.  The <literal>WITHOUT OVERLAPS</literal> column
-      must have a range or multirange type.  (Technically, any type is allowed
-      whose default GiST opclass includes an overlaps operator.  See the
-      <literal>stratnum</literal> support function under <xref
-      linkend="gist-extensibility"/> for details.)  The non-<literal>WITHOUT
-      OVERLAPS</literal> columns of the constraint can be any type that can be
-      compared for equality in a GiST index.  By default, only range types are
-      supported, but you can use other types by adding the <xref
-      linkend="btree-gist"/> extension (which is the expected way to use this
-      feature).
+      must have a range or multirange type.  Empty ranges/multiranges are
+      not permitted.  The non-<literal>WITHOUT OVERLAPS</literal> columns of
+      the constraint can be any type that can be compared for equality in a
+      GiST index.  By default, only range types are supported, but you can use
+      other types by adding the <xref linkend="btree-gist"/> extension (which
+      is the expected way to use this feature).
      </para>
 
      <para>
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 5c48e5728e9..2ffdd7a3ccc 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1394,7 +1394,8 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId,
 							oldInfo->ii_NullsNotDistinct,
 							false,	/* not ready for inserts */
 							true,
-							indexRelation->rd_indam->amsummarizing);
+							indexRelation->rd_indam->amsummarizing,
+							oldInfo->ii_WithoutOverlaps);
 
 	/*
 	 * Extract the list of column names and the column numbers for the new
@@ -2431,7 +2432,8 @@ BuildIndexInfo(Relation index)
 					   indexStruct->indnullsnotdistinct,
 					   indexStruct->indisready,
 					   false,
-					   index->rd_indam->amsummarizing);
+					   index->rd_indam->amsummarizing,
+					   indexStruct->indisexclusion && indexStruct->indisunique);
 
 	/* fill in attribute numbers */
 	for (i = 0; i < numAtts; i++)
@@ -2490,7 +2492,8 @@ BuildDummyIndexInfo(Relation index)
 					   indexStruct->indnullsnotdistinct,
 					   indexStruct->indisready,
 					   false,
-					   index->rd_indam->amsummarizing);
+					   index->rd_indam->amsummarizing,
+					   indexStruct->indisexclusion && indexStruct->indisunique);
 
 	/* fill in attribute numbers */
 	for (i = 0; i < numAtts; i++)
@@ -3227,7 +3230,6 @@ IndexCheckExclusion(Relation heapRelation,
 	indexInfo->ii_PredicateState = NULL;
 }
 
-
 /*
  * validate_index - support code for concurrent index builds
  *
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 47e139b3828..262a7510485 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -242,7 +242,7 @@ CheckIndexCompatible(Oid oldId,
 	 */
 	indexInfo = makeIndexInfo(numberOfAttributes, numberOfAttributes,
 							  accessMethodId, NIL, NIL, false, false,
-							  false, false, amsummarizing);
+							  false, false, amsummarizing, isWithoutOverlaps);
 	typeIds = palloc_array(Oid, numberOfAttributes);
 	collationIds = palloc_array(Oid, numberOfAttributes);
 	opclassIds = palloc_array(Oid, numberOfAttributes);
@@ -915,7 +915,8 @@ DefineIndex(Oid tableId,
 							  stmt->nulls_not_distinct,
 							  !concurrent,
 							  concurrent,
-							  amissummarizing);
+							  amissummarizing,
+							  stmt->iswithoutoverlaps);
 
 	typeIds = palloc_array(Oid, numberOfAttributes);
 	collationIds = palloc_array(Oid, numberOfAttributes);
diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c
index 59acf67a36a..4307777d62a 100644
--- a/src/backend/executor/execIndexing.c
+++ b/src/backend/executor/execIndexing.c
@@ -114,6 +114,8 @@
 #include "executor/executor.h"
 #include "nodes/nodeFuncs.h"
 #include "storage/lmgr.h"
+#include "utils/multirangetypes.h"
+#include "utils/rangetypes.h"
 #include "utils/snapmgr.h"
 
 /* waitMode argument to check_exclusion_or_unique_constraint() */
@@ -720,6 +722,31 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 		constr_strats = indexInfo->ii_UniqueStrats;
 	}
 
+	/*
+	 * If this is a WITHOUT OVERLAPS constraint,
+	 * we must also forbid empty ranges/multiranges.
+	 * This must happen before we look for NULLs below,
+	 * or a UNIQUE constraint could insert an empty
+	 * range along with a NULL scalar part.
+	 */
+	if (indexInfo->ii_WithoutOverlaps)
+	{
+		/*
+		 * Look up the type from the heap tuple,
+		 * but check the Datum from the index tuple.
+		 */
+		AttrNumber attno = indexInfo->ii_IndexAttrNumbers[indnkeyatts - 1];
+
+		if (!isnull[indnkeyatts - 1])
+		{
+			TupleDesc tupdesc = RelationGetDescr(heap);
+			Form_pg_attribute att = TupleDescAttr(tupdesc, attno - 1);
+			TypeCacheEntry *typcache = lookup_type_cache(att->atttypid, 0);
+			ExecWithoutOverlapsNotEmpty(heap, values[indnkeyatts - 1],
+										typcache->typtype, att->atttypid);
+		}
+	}
+
 	/*
 	 * If any of the input values are NULL, and the index uses the default
 	 * nulls-are-distinct mode, the constraint check is assumed to pass (i.e.,
@@ -1097,3 +1124,36 @@ index_expression_changed_walker(Node *node, Bitmapset *allUpdatedCols)
 	return expression_tree_walker(node, index_expression_changed_walker,
 								  (void *) allUpdatedCols);
 }
+
+/*
+ * ExecWithoutOverlapsNotEmpty - raise an error if the tuple has an empty
+ * range or multirange in the given attribute.
+ */
+void
+ExecWithoutOverlapsNotEmpty(Relation rel, Datum attval, Oid typtype, Oid atttypid)
+{
+	bool isempty;
+	RangeType *r;
+	MultirangeType *mr;
+
+	switch (typtype)
+	{
+		case TYPTYPE_RANGE:
+			r = DatumGetRangeTypeP(attval);
+			isempty = RangeIsEmpty(r);
+			break;
+		case TYPTYPE_MULTIRANGE:
+			mr = DatumGetMultirangeTypeP(attval);
+			isempty = MultirangeIsEmpty(mr);
+			break;
+		default:
+			elog(ERROR, "Got unknown type for WITHOUT OVERLAPS column: %d", atttypid);
+	}
+
+	/* Report a CHECK_VIOLATION */
+	if (isempty)
+		ereport(ERROR,
+				(errcode(ERRCODE_CHECK_VIOLATION),
+				 errmsg("new row for relation \"%s\" contains empty WITHOUT OVERLAPS value",
+						RelationGetRelationName(rel))));
+}
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 61ac172a857..9cac3c1c27b 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -760,7 +760,8 @@ make_ands_implicit(Expr *clause)
 IndexInfo *
 makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
 			  List *predicates, bool unique, bool nulls_not_distinct,
-			  bool isready, bool concurrent, bool summarizing)
+			  bool isready, bool concurrent, bool summarizing,
+			  bool withoutoverlaps)
 {
 	IndexInfo  *n = makeNode(IndexInfo);
 
@@ -775,6 +776,7 @@ makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions,
 	n->ii_IndexUnchanged = false;
 	n->ii_Concurrent = concurrent;
 	n->ii_Summarizing = summarizing;
+	n->ii_WithoutOverlaps = withoutoverlaps;
 
 	/* summarizing indexes cannot contain non-key attributes */
 	Assert(!summarizing || (numkeyattrs == numattrs));
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 53492c644a9..905c986dc6a 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -2397,7 +2397,8 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	 * For UNIQUE and PRIMARY KEY, we just have a list of column names.
 	 *
 	 * Make sure referenced keys exist.  If we are making a PRIMARY KEY index,
-	 * also make sure they are NOT NULL.
+	 * also make sure they are NOT NULL.  For WITHOUT OVERLAPS constraints,
+	 * we make sure the last part is a range or multirange.
 	 */
 	else
 	{
@@ -2409,6 +2410,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			ColumnDef  *column = NULL;
 			ListCell   *columns;
 			IndexElem  *iparam;
+			Oid			typid = InvalidOid;
 
 			/* Make sure referenced column exists. */
 			foreach(columns, cxt->columns)
@@ -2420,6 +2422,9 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 					break;
 				}
 			}
+			if (!found)
+				column = NULL;
+
 			if (found)
 			{
 				/*
@@ -2475,6 +2480,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 						if (strcmp(key, inhname) == 0)
 						{
 							found = true;
+							typid = inhattr->atttypid;
 
 							/*
 							 * It's tempting to set forced_not_null if the
@@ -2524,6 +2530,45 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				}
 			}
 
+			/* The WITHOUT OVERLAPS part (if any) must be a range or multirange type. */
+			if (constraint->without_overlaps && lc == list_last_cell(constraint->keys))
+			{
+				if (!found && cxt->isalter)
+				{
+					/*
+					 * Look up the column type on existing table.
+					 * If we can't find it, let things fail in DefineIndex.
+					 */
+					Relation rel = cxt->rel;
+					for (int i = 0; i < rel->rd_att->natts; i++)
+					{
+						Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+						const char *attname;
+
+						if (attr->attisdropped)
+							break;
+
+						attname = NameStr(attr->attname);
+						if (strcmp(attname, key) == 0)
+						{
+							typid = attr->atttypid;
+							break;
+						}
+					}
+				}
+				if (found)
+				{
+					if (!OidIsValid(typid) && column)
+						typid = typenameTypeId(NULL, column->typeName);
+
+					if (!OidIsValid(typid) || !(type_is_range(typid) || type_is_multirange(typid)))
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range or multirange type", key),
+								 parser_errposition(cxt->pstate, constraint->location)));
+				}
+			}
+
 			/* OK, add it to the index definition */
 			iparam = makeNode(IndexElem);
 			iparam->name = pstrdup(key);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 9770752ea3c..315dd225efc 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -642,6 +642,8 @@ extern void check_exclusion_constraint(Relation heap, Relation index,
 									   ItemPointer tupleid,
 									   const Datum *values, const bool *isnull,
 									   EState *estate, bool newIndex);
+extern void ExecWithoutOverlapsNotEmpty(Relation rel, Datum attval,
+										Oid typtype, Oid typtypid);
 
 /*
  * prototypes from functions in execReplication.c
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cac684d9b3a..15a42d47dfe 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -202,6 +202,7 @@ typedef struct IndexInfo
 	bool		ii_Concurrent;
 	bool		ii_BrokenHotChain;
 	bool		ii_Summarizing;
+	bool		ii_WithoutOverlaps;
 	int			ii_ParallelWorkers;
 	Oid			ii_Am;
 	void	   *ii_AmCache;
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 5209d3de89c..0765e5c57b4 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -98,7 +98,7 @@ extern IndexInfo *makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid,
 								List *expressions, List *predicates,
 								bool unique, bool nulls_not_distinct,
 								bool isready, bool concurrent,
-								bool summarizing);
+								bool summarizing, bool withoutoverlaps);
 
 extern Node *makeStringConst(char *str, int location);
 extern DefElem *makeDefElem(char *name, Node *arg, int location);
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 94234758598..93b7c140158 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -27,8 +27,9 @@ CREATE TABLE temporal_rng (
 	valid_at TEXT,
 	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 );
-ERROR:  data type text has no default operator class for access method "gist"
-HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range or multirange type
+LINE 4:  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU...
+         ^
 -- PK with one column plus a range:
 CREATE TABLE temporal_rng (
 	-- Since we can't depend on having btree_gist here,
@@ -238,8 +239,9 @@ CREATE TABLE temporal_rng3 (
 	valid_at TEXT,
 	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
 );
-ERROR:  data type text has no default operator class for access method "gist"
-HINT:  You must specify an operator class for the index or define a default operator class for the data type.
+ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range or multirange type
+LINE 4:  CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV...
+         ^
 -- UNIQUE with one column plus a range:
 CREATE TABLE temporal_rng3 (
 	id int4range,
@@ -393,6 +395,12 @@ BEGIN;
 ERROR:  could not create exclusion constraint "temporal_rng_pk"
 DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
+  ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  new row for relation "temporal_rng" contains empty WITHOUT OVERLAPS value
+ROLLBACK;
 ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_rng;
 --
@@ -413,6 +421,9 @@ DETAIL:  Failing row contains (null, [2018-01-01,2018-01-05)).
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
 ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
 DETAIL:  Failing row contains ([3,4), null).
+-- rejects empty:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
+ERROR:  new row for relation "temporal_rng" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_rng ORDER BY id, valid_at;
   id   |        valid_at         
 -------+-------------------------
@@ -471,6 +482,12 @@ SET     id = '[1,2)',
 WHERE   id = '[21,22)';
 ERROR:  null value in column "valid_at" of relation "temporal_rng" violates not-null constraint
 DETAIL:  Failing row contains ([1,2), null).
+-- rejects empty:
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = 'empty'
+WHERE   id = '[21,22)';
+ERROR:  new row for relation "temporal_rng" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_rng ORDER BY id, valid_at;
    id    |        valid_at         
 ---------+-------------------------
@@ -503,6 +520,12 @@ BEGIN;
 ERROR:  could not create exclusion constraint "temporal_rng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', 'empty');
+  ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ERROR:  new row for relation "temporal_rng3" contains empty WITHOUT OVERLAPS value
+ROLLBACK;
 ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_rng3;
 --
@@ -519,6 +542,9 @@ INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
 INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
 ERROR:  conflicting key value violates exclusion constraint "temporal_rng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
+-- rejects empty:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', 'empty');
+ERROR:  new row for relation "temporal_rng3" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
   id   |        valid_at         
 -------+-------------------------
@@ -576,6 +602,17 @@ SET     valid_at = daterange('2018-03-01', '2018-05-05')
 WHERE   id = '[1,2)' AND valid_at IS NULL;
 ERROR:  conflicting key value violates exclusion constraint "temporal_rng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), [2018-03-01,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+-- rejects empty:
+UPDATE  temporal_rng3
+SET     valid_at = 'empty'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  new row for relation "temporal_rng3" contains empty WITHOUT OVERLAPS value
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_rng3
+SET     id = NULL,
+        valid_at = 'empty'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  new row for relation "temporal_rng3" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
   id   |        valid_at         
 -------+-------------------------
@@ -606,6 +643,12 @@ BEGIN;
 ERROR:  could not create exclusion constraint "temporal_mltrng_pk"
 DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}) conflicts with key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}).
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', '{}');
+  ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  new row for relation "temporal_mltrng" contains empty WITHOUT OVERLAPS value
+ROLLBACK;
 ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_mltrng;
 --
@@ -626,6 +669,9 @@ DETAIL:  Failing row contains (null, {[2018-01-01,2018-01-05)}).
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', NULL);
 ERROR:  null value in column "valid_at" of relation "temporal_mltrng" violates not-null constraint
 DETAIL:  Failing row contains ([3,4), null).
+-- rejects empty:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', '{}');
+ERROR:  new row for relation "temporal_mltrng" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
   id   |         valid_at          
 -------+---------------------------
@@ -684,6 +730,12 @@ SET     id = '[1,2)',
 WHERE   id = '[21,22)';
 ERROR:  null value in column "valid_at" of relation "temporal_mltrng" violates not-null constraint
 DETAIL:  Failing row contains ([1,2), null).
+-- rejects empty:
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = '{}'
+WHERE   id = '[21,22)';
+ERROR:  new row for relation "temporal_mltrng" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
    id    |         valid_at          
 ---------+---------------------------
@@ -716,6 +768,12 @@ BEGIN;
 ERROR:  could not create exclusion constraint "temporal_mltrng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}) conflicts with key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}).
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', '{}');
+  ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ERROR:  new row for relation "temporal_mltrng3" contains empty WITHOUT OVERLAPS value
+ROLLBACK;
 ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_mltrng3;
 --
@@ -732,6 +790,9 @@ INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
 INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
 ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), {[2018-01-01,2018-01-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-01-02,2018-02-03)}).
+-- rejects empty:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', '{}');
+ERROR:  new row for relation "temporal_mltrng3" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
   id   |         valid_at          
 -------+---------------------------
@@ -789,6 +850,17 @@ SET     valid_at = datemultirange(daterange('2018-03-01', '2018-05-05'))
 WHERE   id = '[1,2)' AND valid_at IS NULL;
 ERROR:  conflicting key value violates exclusion constraint "temporal_mltrng3_uq"
 DETAIL:  Key (id, valid_at)=([1,2), {[2018-03-01,2018-05-05)}) conflicts with existing key (id, valid_at)=([1,2), {[2018-03-03,2018-04-04)}).
+-- rejects empty:
+UPDATE  temporal_mltrng3
+SET     valid_at = '{}'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  new row for relation "temporal_mltrng3" contains empty WITHOUT OVERLAPS value
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_mltrng3
+SET     id = NULL,
+        valid_at = '{}'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+ERROR:  new row for relation "temporal_mltrng3" contains empty WITHOUT OVERLAPS value
 SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
   id   |         valid_at          
 -------+---------------------------
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 36b3e6dc02a..aef130ee655 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -264,6 +264,11 @@ BEGIN;
   INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
   ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
+  ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
 ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_rng;
 
@@ -281,6 +286,8 @@ INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', daterange('2018-01-01',
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
 INSERT INTO temporal_rng (id, valid_at) VALUES (NULL, daterange('2018-01-01', '2018-01-05'));
 INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', NULL);
+-- rejects empty:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[3,4)', 'empty');
 SELECT * FROM temporal_rng ORDER BY id, valid_at;
 
 --
@@ -319,6 +326,11 @@ UPDATE  temporal_rng
 SET     id = '[1,2)',
         valid_at = NULL
 WHERE   id = '[21,22)';
+-- rejects empty:
+UPDATE  temporal_rng
+SET     id = '[1,2)',
+        valid_at = 'empty'
+WHERE   id = '[21,22)';
 SELECT * FROM temporal_rng ORDER BY id, valid_at;
 
 --
@@ -345,6 +357,11 @@ BEGIN;
   INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
   ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', 'empty');
+  ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
 ALTER TABLE temporal_rng3 ADD CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_rng3;
 
@@ -362,6 +379,8 @@ INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', NULL);
 
 -- should fail:
 INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[1,2)', daterange('2018-01-01', '2018-01-05'));
+-- rejects empty:
+INSERT INTO temporal_rng3 (id, valid_at) VALUES ('[3,4)', 'empty');
 SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
 
 --
@@ -399,6 +418,15 @@ SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
 UPDATE  temporal_rng3
 SET     valid_at = daterange('2018-03-01', '2018-05-05')
 WHERE   id = '[1,2)' AND valid_at IS NULL;
+-- rejects empty:
+UPDATE  temporal_rng3
+SET     valid_at = 'empty'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_rng3
+SET     id = NULL,
+        valid_at = 'empty'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
 SELECT * FROM temporal_rng3 ORDER BY id, valid_at;
 DROP TABLE temporal_rng3;
 
@@ -421,6 +449,11 @@ BEGIN;
   INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
   ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', '{}');
+  ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
 ALTER TABLE temporal_mltrng ADD CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_mltrng;
 
@@ -438,6 +471,8 @@ INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', datemultirange(dater
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
 INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', NULL);
+-- rejects empty:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[3,4)', '{}');
 SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
 
 --
@@ -476,6 +511,11 @@ UPDATE  temporal_mltrng
 SET     id = '[1,2)',
         valid_at = NULL
 WHERE   id = '[21,22)';
+-- rejects empty:
+UPDATE  temporal_mltrng
+SET     id = '[1,2)',
+        valid_at = '{}'
+WHERE   id = '[21,22)';
 SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
 
 --
@@ -502,6 +542,11 @@ BEGIN;
   INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
   ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 ROLLBACK;
+-- rejects empty:
+BEGIN;
+  INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', '{}');
+  ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
 ALTER TABLE temporal_mltrng3 ADD CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DELETE FROM temporal_mltrng3;
 
@@ -519,6 +564,8 @@ INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', NULL);
 
 -- should fail:
 INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-01-01', '2018-01-05')));
+-- rejects empty:
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[3,4)', '{}');
 SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
 --
@@ -556,6 +603,15 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 UPDATE  temporal_mltrng3
 SET     valid_at = datemultirange(daterange('2018-03-01', '2018-05-05'))
 WHERE   id = '[1,2)' AND valid_at IS NULL;
+-- rejects empty:
+UPDATE  temporal_mltrng3
+SET     valid_at = '{}'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_mltrng3
+SET     id = NULL,
+        valid_at = '{}'
+WHERE   id = '[1,2)' AND valid_at IS NULL;
 SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 DROP TABLE temporal_mltrng3;
 
-- 
2.42.0

From 9b57dfc79480be40d8c331031d0366a85c677414 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 24 Mar 2024 07:37:13 +0100
Subject: [PATCH v37 4/8] Add temporal FOREIGN KEY contraints

Add PERIOD clause to foreign key constraint definitions.  This is
supported for range and multirange types.  Temporal foreign keys check
for range containment instead of equality.

This feature matches the behavior of the SQL standard temporal foreign
keys, but it works on PostgreSQL's native ranges instead of SQL's
"periods", which don't exist in PostgreSQL (yet).

Reference actions ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}
are not supported yet.
---
 .../btree_gist/expected/without_overlaps.out  |   48 +
 contrib/btree_gist/sql/without_overlaps.sql   |   28 +
 doc/src/sgml/catalogs.sgml                    |    3 +-
 doc/src/sgml/ref/create_table.sgml            |   45 +-
 src/backend/catalog/pg_constraint.c           |   58 +
 src/backend/commands/indexcmds.c              |   31 +-
 src/backend/commands/tablecmds.c              |  226 +++-
 src/backend/parser/gram.y                     |   45 +-
 src/backend/utils/adt/ri_triggers.c           |  168 ++-
 src/backend/utils/adt/ruleutils.c             |   17 +-
 src/include/catalog/pg_constraint.h           |   21 +-
 src/include/commands/defrem.h                 |    2 +-
 src/include/nodes/parsenodes.h                |    2 +
 src/include/parser/kwlist.h                   |    1 +
 .../regress/expected/without_overlaps.out     | 1089 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 1036 +++++++++++++++-
 16 files changed, 2704 insertions(+), 116 deletions(-)

index be52c522e89..18856900ded 100644
+-- key exist but is outside range
+-- key exist but is partly outside range
index 433c609ab22..b1b581fcabc 100644
+-- key exist but is outside range
+-- key exist but is partly outside range
index 1e56edaa70b..bfb97865e18 100644
@@ -2736,7 +2736,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para>
       <para>
        This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
-       (for primary keys and unique constraints).
+       (for primary keys and unique constraints) or <literal>PERIOD</literal>
+       (for foreign keys).
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 9844d23be40..30150eebfa0 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -80,7 +80,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
   UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
 class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -1147,8 +1147,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    <varlistentry id="sql-createtable-parms-references">
     <term><literal>REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH <replaceable class="parameter">matchtype</replaceable> ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal> (column constraint)</term>
 
-   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
-    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
+   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] )
+    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
     [ MATCH <replaceable class="parameter">matchtype</replaceable> ]
     [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
     [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1164,7 +1164,32 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       primary key of the <replaceable class="parameter">reftable</replaceable>
       is used.  Otherwise, the <replaceable class="parameter">refcolumn</replaceable>
       list must refer to the columns of a non-deferrable unique or primary key
-      constraint or be the columns of a non-partial unique index.  The user
+      constraint or be the columns of a non-partial unique index.
+     </para>
+
+     <para>
+      If the last column is marked with <literal>PERIOD</literal>, it is
+      treated in a special way.  While the non-<literal>PERIOD</literal>
+      columns are compared for equality (and there must be at least one of
+      them), the <literal>PERIOD</literal> column is not.  Instead, the
+      constraint is considered satisfied if the referenced table has matching
+      records (based on the non-<literal>PERIOD</literal> parts of the key)
+      whose combined <literal>PERIOD</literal> values completely cover the
+      referencing record's.  In other words, the reference must have a
+      referent for its entire duration.  This column must be a range or
+      multirange type.  In addition, the referenced table must have a primary
+      key or unique constraint declared with <literal>WITHOUT
+      OVERLAPS</literal>. Finally, if the foreign key has a PERIOD
+      <replaceable class="parameter">column_name</replaceable> specification
+      the corresponding <replaceable class="parameter">refcolumn</replaceable>,
+      if present, must also be marked <literal>PERIOD</literal>.  If the
+      <replaceable class="parameter">refcolumn</replaceable> clause is omitted,
+      and thus the reftable's primary key constraint chosen, the primary key
+      must have its final column marked <literal>WITHOUT OVERLAPS</literal>.
+     </para>
+
+     <para>
+      The user
       must have <literal>REFERENCES</literal> permission on the referenced
       table (either the whole table, or the specific referenced columns).  The
       addition of a foreign key constraint requires a
@@ -1238,6 +1263,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           values of the referencing column(s) to the new values of the
           referenced columns, respectively.
          </para>
+
+         <para>
+          In a temporal foreign key, this option is not supported.
+         </para>
         </listitem>
        </varlistentry>
 
@@ -1249,6 +1278,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           referencing columns, to null. A subset of columns can only be
           specified for <literal>ON DELETE</literal> actions.
          </para>
+
+         <para>
+          In a temporal foreign key, this option is not supported.
+         </para>
         </listitem>
        </varlistentry>
 
@@ -1262,6 +1295,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           (There must be a row in the referenced table matching the default
           values, if they are not null, or the operation will fail.)
          </para>
+
+         <para>
+          In a temporal foreign key, this option is not supported.
+         </para>
         </listitem>
        </varlistentry>
       </variablelist>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9be050ccee8..1e2df031a84 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
@@ -1349,6 +1350,63 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 	*numfks = numkeys;
 }
 
+/*
+ * FindFkPeriodOpers -
+ *
+ * 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.
+ * Everything else is an output: containedbyoperoid is the ContainedBy operator for
+ * types matching the PERIOD element.
+ * aggedcontainedbyoperoid is also a ContainedBy operator,
+ * but one whose rhs is a multirange.
+ * That way foreign keys can compare fkattr <@ range_agg(pkattr).
+ */
+void
+FindFKPeriodOpers(Oid opclass,
+				  Oid *containedbyoperoid,
+				  Oid *aggedcontainedbyoperoid)
+{
+	Oid			opfamily = InvalidOid;
+	Oid			opcintype = InvalidOid;
+	StrategyNumber strat;
+
+	/* Make sure we have a range or multirange. */
+	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+	{
+		if (opcintype != ANYRANGEOID && opcintype != ANYMULTIRANGEOID)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("invalid type for PERIOD part of foreign key"),
+					errdetail("Only range and multirange are supported."));
+
+	}
+	else
+		elog(ERROR, "cache lookup failed for opclass %u", opclass);
+
+	/*
+	 * Look up the ContainedBy operator whose lhs and rhs are the opclass's
+	 * type. We use this to optimize RI checks: if the new value includes all
+	 * of the old value, then we can treat the attribute as if it didn't
+	 * change, and skip the RI check.
+	 */
+	strat = RTContainedByStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass,
+									 InvalidOid,
+									 containedbyoperoid,
+									 &strat);
+
+	/*
+	 * Now look up the ContainedBy operator. Its left arg must be the type of
+	 * the column (or rather of the opclass). Its right arg must match the
+	 * return type of the support proc.
+	 */
+	strat = RTContainedByStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass,
+									 ANYMULTIRANGEOID,
+									 aggedcontainedbyoperoid,
+									 &strat);
+}
+
 /*
  * Determine whether a relation can be proven functionally dependent on
  * a set of grouping columns.  If so, return true and add the pg_constraint
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 262a7510485..29c2a0edf79 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2190,7 +2190,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				strat = RTOverlapStrategyNumber;
 			else
 				strat = RTEqualStrategyNumber;
-			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
+			GetOperatorFromWellKnownStrategy(opclassOids[attn], InvalidOid,
 											 &opid, &strat);
 			indexInfo->ii_ExclusionOps[attn] = opid;
 			indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
@@ -2430,7 +2430,7 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * GetOperatorFromWellKnownStrategy
  *
  * opclass - the opclass to use
- * atttype - the type to ask about
+ * rhstype - the type for the right-hand side, or InvalidOid to use the type of the given opclass.
  * opid - holds the operator we found
  * strat - holds the input and output strategy number
  *
@@ -2443,14 +2443,14 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * InvalidStrategy.
  */
 void
-GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
+GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
 								 Oid *opid, StrategyNumber *strat)
 {
 	Oid			opfamily;
 	Oid			opcintype;
 	StrategyNumber instrat = *strat;
 
-	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber || instrat == RTContainedByStrategyNumber);
 
 	*opid = InvalidOid;
 
@@ -2473,16 +2473,21 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 			ereport(ERROR,
 					errcode(ERRCODE_UNDEFINED_OBJECT),
-					instrat == RTEqualStrategyNumber ?
-					errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-					errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+					instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
+					instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
+					instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
 					errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
 							  instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
-
-			ReleaseSysCache(tuple);
 		}

 	if (!OidIsValid(*opid))
@@ -2495,9 +2500,9 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 		ereport(ERROR,
 				errcode(ERRCODE_UNDEFINED_OBJECT),
-				instrat == RTEqualStrategyNumber ?
-				errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
-				errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
+				instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
+				instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
+				instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
 				errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
 						  NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
 	}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c39e8bc161d..b1716cdbce1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@
 
 #include "access/attmap.h"
 #include "access/genam.h"
+#include "access/gist.h"
 #include "access/heapam.h"
 #include "access/heapam_xlog.h"
 #include "access/multixact.h"
@@ -215,6 +216,7 @@ typedef struct NewConstraint
 	ConstrType	contype;		/* CHECK or FOREIGN */
 	Oid			refrelid;		/* PK rel, if FOREIGN */
 	Oid			refindid;		/* OID of PK's index, if FOREIGN */
+	bool		conwithperiod;	/* Whether the new FOREIGN KEY uses PERIOD */
 	Oid			conid;			/* OID of pg_constraint entry, if FOREIGN */
 	Node	   *qual;			/* Check expr or CONSTR_FOREIGN Constraint */
 	ExprState  *qualstate;		/* Execution state for CHECK expr */
@@ -389,16 +391,17 @@ static int	transformColumnNameList(Oid relId, List *colList,
 static int	transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 									   List **attnamelist,
 									   int16 *attnums, Oid *atttypids,
-									   Oid *opclasses);
+									   Oid *opclasses, bool *pk_has_without_overlaps);
 static Oid	transformFkeyCheckAttrs(Relation pkrel,
 									int numattrs, int16 *attnums,
-									Oid *opclasses);
+									bool with_period, Oid *opclasses,
+									bool *pk_has_without_overlaps);
 static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
 static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
 									 Oid *funcid);
 static void validateForeignKeyConstraint(char *conname,
 										 Relation rel, Relation pkrel,
-										 Oid pkindOid, Oid constraintOid);
+										 Oid pkindOid, Oid constraintOid, bool hasperiod);
 static void CheckAlterTableIsSafe(Relation rel);
 static void ATController(AlterTableStmt *parsetree,
 						 Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
@@ -510,7 +513,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
 											Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 											int numfkdelsetcols, int16 *fkdelsetcols,
 											bool old_check_ok,
-											Oid parentDelTrigger, Oid parentUpdTrigger);
+											Oid parentDelTrigger, Oid parentUpdTrigger,
+											bool with_period);
 static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
@@ -520,7 +524,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
 									Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 									int numfkdelsetcols, int16 *fkdelsetcols,
 									bool old_check_ok, LOCKMODE lockmode,
-									Oid parentInsTrigger, Oid parentUpdTrigger);
+									Oid parentInsTrigger, Oid parentUpdTrigger,
+									bool with_period);
+
 static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
 									   Relation partitionRel);
 static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -5945,7 +5951,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9586,6 +9593,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	Oid			ppeqoperators[INDEX_MAX_KEYS] = {0};
 	Oid			ffeqoperators[INDEX_MAX_KEYS] = {0};
 	int16		fkdelsetcols[INDEX_MAX_KEYS] = {0};
+	bool		with_period;
+	bool		pk_has_without_overlaps;
 	int			i;
 	int			numfks,
 				numpks,
@@ -9680,6 +9689,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfks = transformColumnNameList(RelationGetRelid(rel),
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
+	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"));
 
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
@@ -9699,18 +9713,40 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
 											&fkconstraint->pk_attrs,
 											pkattnum, pktypoid,
-											opclasses);
+											opclasses, &pk_has_without_overlaps);
+
+		/* If the primary key uses WITHOUT OVERLAPS, the fk must use PERIOD */
+		if (pk_has_without_overlaps && !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"));
 	}
 	else
 	{
 		numpks = transformColumnNameList(RelationGetRelid(pkrel),
 										 fkconstraint->pk_attrs,
 										 pkattnum, pktypoid);
+
+		/* Since we got pk_attrs, one should be a period. */
+		if (with_period && !fkconstraint->pk_with_period)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_FOREIGN_KEY),
+					errmsg("foreign key uses PERIOD on the referencing table but not the referenced table"));
+
 		/* Look for an index matching the column list */
 		indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
-										   opclasses);
+										   with_period, opclasses, &pk_has_without_overlaps);
 	}
 
+	/*
+	 * If the referenced primary key has WITHOUT OVERLAPS, the foreign key
+	 * must use PERIOD.
+	 */
+	if (pk_has_without_overlaps && !with_period)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_FOREIGN_KEY),
+				errmsg("foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS"));
+
 	/*
 	 * Now we can check permissions.
 	 */
@@ -9744,6 +9780,28 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
+	/*
+	 * Some actions are currently unsupported for foreign keys using PERIOD.
+	 */
+	if (fkconstraint->fk_with_period)
+	{
+		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE ||
+			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
+			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("unsupported %s action for foreign key constraint using PERIOD",
+						   "ON UPDATE"));
+
+		if (fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE ||
+			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
+			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("unsupported %s action for foreign key constraint using PERIOD",
+						   "ON DELETE"));
+	}
+
 	/*
 	 * Look up the equality operators to use in the constraint.
 	 *
@@ -9790,16 +9848,56 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		opcintype = cla_tup->opcintype;
 		ReleaseSysCache(cla_ht);
 
-		/*
-		 * Check it's a btree; currently this can never fail since no other
-		 * index AMs support unique indexes.  If we ever did have other types
-		 * of unique indexes, we'd need a way to determine which operator
-		 * strategy number is equality.  (Is it reasonable to insist that
-		 * every such index AM use btree's number for equality?)
-		 */
-		if (amid != BTREE_AM_OID)
-			elog(ERROR, "only b-tree indexes are supported for foreign keys");
-		eqstrategy = BTEqualStrategyNumber;
+		if (with_period)
+		{
+			StrategyNumber rtstrategy;
+			bool		for_overlaps = with_period && i == numpks - 1;
+
+			/*
+			 * GiST indexes are required to support temporal foreign keys
+			 * because they combine equals and overlaps.
+			 */
+			if (amid != GIST_AM_OID)
+				elog(ERROR, "only GiST indexes are supported for temporal foreign keys");
+
+			rtstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
+
+			/*
+			 * An opclass can use whatever strategy numbers it wants, so we
+			 * ask the opclass what number it actually uses instead of our RT*
+			 * constants.
+			 */
+			eqstrategy = GistTranslateStratnum(opclasses[i], rtstrategy);
+			if (eqstrategy == InvalidStrategy)
+			{
+				HeapTuple	tuple;
+
+				tuple = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
+				if (!HeapTupleIsValid(tuple))
+					elog(ERROR, "cache lookup failed for operator class %u", opclasses[i]);
+
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						for_overlaps
+						? errmsg("could not identify an overlaps operator for foreign key")
+						: errmsg("could not identify an equality operator for foreign key"),
+						errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
+								  rtstrategy, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
+			}
+		}
+		else
+		{
+			/*
+			 * Check it's a btree; currently this can never fail since no
+			 * other index AMs support unique indexes.  If we ever did have
+			 * other types of unique indexes, we'd need a way to determine
+			 * which operator strategy number is equality.  (We could use
+			 * something like GistTranslateStratnum.)
+			 */
+			if (amid != BTREE_AM_OID)
+				elog(ERROR, "only b-tree indexes are supported for foreign keys");
+			eqstrategy = BTEqualStrategyNumber;
+		}
 
 		/*
 		 * There had better be a primary equality operator for the index.
@@ -9949,6 +10047,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		ffeqoperators[i] = ffeqop;
 	}
 
+	/*
+	 * For FKs with PERIOD we need additional operators to check whether the
+	 * referencing row's range is contained by the aggregated ranges of the
+	 * referenced row(s). For rangetypes and multirangetypes this is
+	 * fk.periodatt <@ range_agg(pk.periodatt). Those are the only types we
+	 * support for now. FKs will look these up at "runtime", but we should
+	 * make sure the lookup works here, even if we don't use the values.
+	 */
+	if (with_period)
+	{
+		Oid			periodoperoid;
+		Oid			aggedperiodoperoid;
+
+		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
+	}
+
 	/*
 	 * Create all the constraint and trigger objects, recursing to partitions
 	 * as necessary.  First handle the referenced side.
@@ -9965,7 +10079,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 with_period);
 
@@ -9981,7 +10096,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
+							with_period);
 
 	/*
 	 * Done.  Close pk table, but keep lock until we've committed.
@@ -10066,7 +10182,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 					   Oid *ppeqoperators, Oid *ffeqoperators,
 					   int numfkdelsetcols, int16 *fkdelsetcols,
 					   bool old_check_ok,
-					   Oid parentDelTrigger, Oid parentUpdTrigger)
+					   Oid parentDelTrigger, Oid parentUpdTrigger,
+					   bool with_period)
 {
 	ObjectAddress address;
 	Oid			constrOid;
@@ -10152,7 +10269,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
-									  false,	/* conPeriod */
+									  with_period,	/* conPeriod */
 									  false);	/* is_internal */
 
@@ -10228,7 +10345,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
+								   with_period);
 
 			/* Done -- clean up (but keep the lock) */
 			table_close(partRel, NoLock);
@@ -10286,7 +10404,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 						Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
 						int numfkdelsetcols, int16 *fkdelsetcols,
 						bool old_check_ok, LOCKMODE lockmode,
-						Oid parentInsTrigger, Oid parentUpdTrigger)
+						Oid parentInsTrigger, Oid parentUpdTrigger,
+						bool with_period)
 {
 	Oid			insertTriggerOid,
 				updateTriggerOid;
@@ -10334,6 +10453,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 			newcon->refrelid = RelationGetRelid(pkrel);
 			newcon->refindid = indexOid;
 			newcon->conid = parentConstr;
+			newcon->conwithperiod = fkconstraint->fk_with_period;
 			newcon->qual = (Node *) fkconstraint;
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10451,7 +10571,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conPeriod */
+									  with_period,	/* conPeriod */
 									  false);
 
 			/*
@@ -10482,7 +10602,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									with_period);
 
 			table_close(partition, NoLock);
 		}
@@ -10718,7 +10839,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conperiod);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10811,6 +10933,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		with_period;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10926,6 +11049,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
 		indexOid = constrForm->conindid;
+		with_period = constrForm->conperiod;
 		constrOid =
 			CreateConstraintEntry(fkconstraint->conname,
 								  constrForm->connamespace,
@@ -10957,7 +11081,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
-								  false,	/* conPeriod */
+								  with_period,	/* conPeriod */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -10991,7 +11115,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								with_period);
 		table_close(pkrel, NoLock);
 	}
 
@@ -11802,7 +11927,8 @@ transformColumnNameList(Oid relId, List *colList,
  *
  *	Look up the names, attnums, and types of the primary key attributes
  *	for the pkrel.  Also return the index OID and index opclasses of the
- *	index supporting the primary key.
+ *	index supporting the primary key.  Also return whether the index has
+ *	WITHOUT OVERLAPS.
  *
  *	All parameters except pkrel are output parameters.  Also, the function
  *	return value is the number of attributes in the primary key.
@@ -11813,7 +11939,7 @@ static int
 transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 						   List **attnamelist,
 						   int16 *attnums, Oid *atttypids,
-						   Oid *opclasses)
+						   Oid *opclasses, bool *pk_has_without_overlaps)
 {
 	List	   *indexoidlist;
 	ListCell   *indexoidscan;
@@ -11891,6 +12017,8 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 							   makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
 	}
 
+	*pk_has_without_overlaps = indexStruct->indisexclusion;
+
 	ReleaseSysCache(indexTuple);
 
 	return i;
@@ -11904,14 +12032,16 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
  *
  *	Returns the OID of the unique index supporting the constraint and
  *	populates the caller-provided 'opclasses' array with the opclasses
- *	associated with the index columns.
+ *	associated with the index columns.  Also sets whether the index
+ *	uses WITHOUT OVERLAPS.
  *
  *	Raises an ERROR on validation failure.
  */
 static Oid
 transformFkeyCheckAttrs(Relation pkrel,
 						int numattrs, int16 *attnums,
-						Oid *opclasses)
+						bool with_period, Oid *opclasses,
+						bool *pk_has_without_overlaps)
 {
 	Oid			indexoid = InvalidOid;
 	bool		found = false;
@@ -11958,12 +12088,12 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
-		 * Must have the right number of columns; must be unique and not a
-		 * partial index; forget it if there are any expressions, too. Invalid
-		 * indexes are out as well.
+		 * Must have the right number of columns; must be unique (or if
+		 * temporal then exclusion instead) and not a partial index; forget it
+		 * if there are any expressions, too. Invalid indexes are out as well.
 		 */
 		if (indexStruct->indnkeyatts == numattrs &&
-			indexStruct->indisunique &&
+			(with_period ? indexStruct->indisexclusion : indexStruct->indisunique) &&
 			indexStruct->indisvalid &&
 			heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) &&
 			heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -12001,6 +12131,13 @@ transformFkeyCheckAttrs(Relation pkrel,
 				if (!found)
 					break;
 			}
+			/* The last attribute in the index must be the PERIOD FK part */
+			if (found && with_period)
+			{
+				int16		periodattnum = attnums[numattrs - 1];
+
+				found = (periodattnum == indexStruct->indkey.values[numattrs - 1]);
+			}
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12016,6 +12153,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 				found_deferrable = true;
 				found = false;
 			}
+
+			/* We need to know whether the index has WITHOUT OVERLAPS */
+			if (found)
+				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
 		ReleaseSysCache(indexTuple);
 		if (found)
@@ -12110,7 +12251,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool hasperiod)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12138,9 +12280,11 @@ validateForeignKeyConstraint(char *conname,
 
 	/*
 	 * See if we can do it with a single LEFT JOIN query.  A false result
-	 * indicates we must proceed with the fire-the-trigger method.
+	 * indicates we must proceed with the fire-the-trigger method. We can't do
+	 * a LEFT JOIN for temporal FKs yet, but we can once we support temporal
+	 * left joins.
 	 */
-	if (RI_Initial_Check(&trig, rel, pkrel))
+	if (!hasperiod && RI_Initial_Check(&trig, rel, pkrel))
 		return;
 
 	/*
@@ -12291,6 +12435,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+
 	switch (fkconstraint->fk_del_action)
 	{
 		case FKCONSTR_ACTION_NOACTION:
@@ -12351,6 +12496,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+
 	switch (fkconstraint->fk_upd_action)
 	{
 		case FKCONSTR_ACTION_NOACTION:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index eafa290b88b..4addf4d533e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -525,12 +525,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				 SetResetClause FunctionSetResetClause
 
 %type <node>	TableElement TypedTableElement ConstraintElem DomainConstraintElem TableFuncElement
@@ -764,7 +765,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
-	PLACING PLAN PLANS POLICY
+	PERIOD PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -4282,21 +4283,31 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
 					n->contype = CONSTR_FOREIGN;
 					n->location = @1;
-					n->pktable = $7;
+					n->pktable = $8;
 					n->fk_attrs = $4;
-					n->pk_attrs = $8;
-					n->fk_matchtype = $9;
-					n->fk_upd_action = ($10)->updateAction->action;
-					n->fk_del_action = ($10)->deleteAction->action;
-					n->fk_del_set_cols = ($10)->deleteAction->cols;
-					processCASbits($11, @11, "FOREIGN KEY",
+					if ($5)
+					{
+						n->fk_attrs = lappend(n->fk_attrs, $5);
+						n->fk_with_period = true;
+					}
+					n->pk_attrs = linitial($9);
+					if (lsecond($9))
+					{
+						n->pk_attrs = lappend(n->pk_attrs, lsecond($9));
+						n->pk_with_period = true;
+					}
+					n->fk_matchtype = $10;
+					n->fk_upd_action = ($11)->updateAction->action;
+					n->fk_del_action = ($11)->deleteAction->action;
+					n->fk_del_set_cols = ($11)->deleteAction->cols;
+					processCASbits($12, @12, "FOREIGN KEY",
 								   &n->deferrable, &n->initdeferred,
 								   &n->skip_validation, NULL,
 								   yyscanner);
@@ -4378,6 +4389,16 @@ columnList:
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
 		;
 
+optionalPeriodName:
+			',' PERIOD columnElem { $$ = $3; }
+			| /*EMPTY*/               { $$ = NULL; }
+	;
+
+opt_column_and_period_list:
+			'(' columnList optionalPeriodName ')'			{ $$ = list_make2($2, $3); }
+			| /*EMPTY*/								{ $$ = list_make2(NIL, NULL); }
+		;
+
 columnElem: ColId
 				{
 					$$ = (Node *) makeString($1);
@@ -17772,6 +17793,7 @@ unreserved_keyword:
 			| PASSING
 			| PASSWORD
 			| PATH
+			| PERIOD
 			| PLAN
 			| PLANS
 			| POLICY
@@ -18398,6 +18420,7 @@ bare_label_keyword:
 			| PASSING
 			| PASSWORD
 			| PATH
+			| PERIOD
 			| PLACING
 			| PLAN
 			| PLANS
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 62601a6d80c..a2cc837ebf9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -30,6 +30,7 @@
 #include "access/xact.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_proc.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
@@ -45,6 +46,7 @@
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/rangetypes.h"
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/ruleutils.h"
@@ -96,6 +98,9 @@
  *
  * Information extracted from an FK pg_constraint entry.  This is cached in
  * ri_constraint_cache.
+ *
+ * Note that pf/pp/ff_eq_oprs may hold the overlaps operator instead of equals
+ * for the PERIOD part of a temporal foreign key.
  */
 typedef struct RI_ConstraintInfo
 {
@@ -115,12 +120,15 @@ typedef struct RI_ConstraintInfo
 	int16		confdelsetcols[RI_MAX_NUMKEYS]; /* attnums of cols to set on
 												 * delete */
 	char		confmatchtype;	/* foreign key's match type */
+	bool		hasperiod;		/* if the foreign key uses PERIOD */
 	int			nkeys;			/* number of key columns */
 	int16		pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */
 	int16		fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */
 	Oid			pf_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = FK) */
 	Oid			pp_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = PK) */
 	Oid			ff_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (FK = FK) */
+	Oid			period_contained_by_oper;	/* anyrange <@ anyrange */
+	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -199,8 +207,8 @@ static void ri_BuildQueryKey(RI_QueryKey *key,
 							 int32 constr_queryno);
 static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 						 const RI_ConstraintInfo *riinfo, bool rel_is_pk);
-static bool ri_AttributesEqual(Oid eq_opr, Oid typeid,
-							   Datum oldvalue, Datum newvalue);
+static bool ri_CompareWithCast(Oid eq_opr, Oid typeid,
+							   Datum lhs, Datum rhs);
 
 static void ri_InitHashTables(void);
 static void InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue);
@@ -362,14 +370,41 @@ RI_FKey_check(TriggerData *trigdata)
 		 *		   FOR KEY SHARE OF x
 		 * The type id's for the $ parameters are those of the
 		 * corresponding FK attributes.
+		 *
+		 * But for temporal FKs we need to make sure
+		 * the FK's range is completely covered.
+		 * So we use this query instead:
+		 *  SELECT 1
+		 *	FROM	(
+		 *		SELECT pkperiodatt AS r
+		 *		FROM   [ONLY] pktable x
+		 *		WHERE  pkatt1 = $1 [AND ...]
+		 *		AND    pkperiodatt && $n
+		 *		FOR KEY SHARE OF x
+		 *	) x1
+		 *  HAVING $n <@ range_agg(x1.r)
+		 * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+		 * we can make this a bit simpler.
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
 		pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
 			"" : "ONLY ";
 		quoteRelationName(pkrelname, pk_rel);
-		appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
-						 pk_only, pkrelname);
+		if (riinfo->hasperiod)
+		{
+			quoteOneName(attname,
+						 RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+			appendStringInfo(&querybuf,
+							 "SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+							 attname, pk_only, pkrelname);
+		}
+		else
+		{
+			appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+							 pk_only, pkrelname);
+		}
 		querysep = "WHERE";
 		for (int i = 0; i < riinfo->nkeys; i++)
 		{
@@ -387,6 +422,18 @@ RI_FKey_check(TriggerData *trigdata)
 			queryoids[i] = fk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->hasperiod)
+		{
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+
+			appendStringInfo(&querybuf, ") x1 HAVING ");
+			sprintf(paramname, "$%d", riinfo->nkeys);
+			ri_GenerateQual(&querybuf, "",
+							paramname, fk_type,
+							riinfo->agged_period_contained_by_oper,
+							"pg_catalog.range_agg", ANYMULTIRANGEOID);
+			appendStringInfo(&querybuf, "(x1.r)");
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -494,14 +541,39 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 		 *		   FOR KEY SHARE OF x
 		 * The type id's for the $ parameters are those of the
 		 * PK attributes themselves.
+		 * But for temporal FKs we need to make sure
+		 * the FK's range is completely covered.
+		 * So we use this query instead:
+		 *  SELECT 1
+		 *  FROM    (
+		 *	  SELECT pkperiodatt AS r
+		 *	  FROM   [ONLY] pktable x
+		 *	  WHERE  pkatt1 = $1 [AND ...]
+		 *	  AND    pkperiodatt && $n
+		 *	  FOR KEY SHARE OF x
+		 *  ) x1
+		 *  HAVING $n <@ range_agg(x1.r)
+		 * Note if FOR KEY SHARE ever allows GROUP BY and HAVING
+		 * we can make this a bit simpler.
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
 		pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
 			"" : "ONLY ";
 		quoteRelationName(pkrelname, pk_rel);
-		appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
-						 pk_only, pkrelname);
+		if (riinfo->hasperiod)
+		{
+			quoteOneName(attname, RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+
+			appendStringInfo(&querybuf,
+							 "SELECT 1 FROM (SELECT %s AS r FROM %s%s x",
+							 attname, pk_only, pkrelname);
+		}
+		else
+		{
+			appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+							 pk_only, pkrelname);
+		}
 		querysep = "WHERE";
 		for (int i = 0; i < riinfo->nkeys; i++)
 		{
@@ -518,6 +590,18 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 			queryoids[i] = pk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->hasperiod)
+		{
+			Oid			fk_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+
+			appendStringInfo(&querybuf, ") x1 HAVING ");
+			sprintf(paramname, "$%d", riinfo->nkeys);
+			ri_GenerateQual(&querybuf, "",
+							paramname, fk_type,
+							riinfo->agged_period_contained_by_oper,
+							"pg_catalog.range_agg", ANYMULTIRANGEOID);
+			appendStringInfo(&querybuf, "(x1.r)");
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -2162,6 +2246,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	riinfo->confupdtype = conForm->confupdtype;
 	riinfo->confdeltype = conForm->confdeltype;
 	riinfo->confmatchtype = conForm->confmatchtype;
+	riinfo->hasperiod = conForm->conperiod;
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2173,6 +2258,20 @@ ri_LoadConstraintInfo(Oid constraintOid)
 							   &riinfo->ndelsetcols,
 							   riinfo->confdelsetcols);
 
+	/*
+	 * For temporal FKs, get the operators and functions we need. We ask the
+	 * opclass of the PK element for these. This all gets cached (as does the
+	 * generated plan), so there's no performance issue.
+	 */
+	if (riinfo->hasperiod)
+	{
+		Oid			opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);
+
+		FindFKPeriodOpers(opclass,
+						  &riinfo->period_contained_by_oper,
+						  &riinfo->agged_period_contained_by_oper);
+	}
+
 	ReleaseSysCache(tup);
 
 	/*
@@ -2784,7 +2883,10 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
 /*
  * ri_KeysEqual -
  *
- * Check if all key values in OLD and NEW are equal.
+ * Check if all key values in OLD and NEW are "equivalent":
+ * For normal FKs we check for equality.
+ * For temporal FKs we check that the PK side is a superset of its old value,
+ * or the FK side is a subset of its old value.
  *
  * Note: at some point we might wish to redefine this as checking for
  * "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be
@@ -2840,13 +2942,25 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 		}
 		else
 		{
+			Oid			eq_opr;
+
+			/*
+			 * When comparing the PERIOD columns we can skip the check
+			 * whenever the referencing column stayed equal or shrank, so test
+			 * with the contained-by operator instead.
+			 */
+			if (riinfo->hasperiod && i == riinfo->nkeys - 1)
+				eq_opr = riinfo->period_contained_by_oper;
+			else
+				eq_opr = riinfo->ff_eq_oprs[i];
+
 			/*
 			 * For the FK table, compare with the appropriate equality
 			 * operator.  Changes that compare equal will still satisfy the
 			 * constraint after the update.
 			 */
-			if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
-									oldvalue, newvalue))
+			if (!ri_CompareWithCast(eq_opr, RIAttType(rel, attnums[i]),
+									newvalue, oldvalue))
 				return false;
 		}
 	}
@@ -2856,29 +2970,31 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,

 /*
- * ri_AttributesEqual -
+ * ri_CompareWithCast -
  *
- * Call the appropriate equality comparison operator for two values.
+ * Call the appropriate comparison operator for two values.
+ * Normally this is equality, but for the PERIOD part of foreign keys
+ * it is ContainedBy, so the order of lhs vs rhs is significant.
  *
  * NB: we have already checked that neither value is null.
  */
 static bool
-ri_AttributesEqual(Oid eq_opr, Oid typeid,
-				   Datum oldvalue, Datum newvalue)
+ri_CompareWithCast(Oid eq_opr, Oid typeid,
+				   Datum lhs, Datum rhs)
 {
 	RI_CompareHashEntry *entry = ri_HashCompareOp(eq_opr, typeid);
 
 	/* Do we need to cast the values? */
 	if (OidIsValid(entry->cast_func_finfo.fn_oid))
 	{
-		oldvalue = FunctionCall3(&entry->cast_func_finfo,
-								 oldvalue,
-								 Int32GetDatum(-1), /* typmod */
-								 BoolGetDatum(false));	/* implicit coercion */
-		newvalue = FunctionCall3(&entry->cast_func_finfo,
-								 newvalue,
-								 Int32GetDatum(-1), /* typmod */
-								 BoolGetDatum(false));	/* implicit coercion */
+		lhs = FunctionCall3(&entry->cast_func_finfo,
+							lhs,
+							Int32GetDatum(-1),	/* typmod */
+							BoolGetDatum(false));	/* implicit coercion */
+		rhs = FunctionCall3(&entry->cast_func_finfo,
+							rhs,
+							Int32GetDatum(-1),	/* typmod */
+							BoolGetDatum(false));	/* implicit coercion */
 	}
 
 	/*
@@ -2892,10 +3008,16 @@ ri_AttributesEqual(Oid eq_opr, Oid typeid,
 	 * open), we'll just use the default collation here, which could lead to
 	 * some false negatives.  All this would break if we ever allow
 	 * database-wide collations to be nondeterministic.
+	 *
+	 * With range/multirangetypes, the collation of the base type is stored as
+	 * part of the rangetype (pg_range.rngcollation), and always used, so
+	 * there is no danger of inconsistency even using a non-equals operator.
+	 * But if we support arbitrary types with PERIOD, we should perhaps just
+	 * always force a re-check.
 	 */
 	return DatumGetBool(FunctionCall2Coll(&entry->eq_opr_finfo,
 										  DEFAULT_COLLATION_OID,
-										  oldvalue, newvalue));
+										  lhs, rhs));
 }
 
 /*
@@ -2950,7 +3072,7 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)
 		 * the cast function to get to the operator's input type.
 		 *
 		 * XXX eventually it would be good to support array-coercion cases
-		 * here and in ri_AttributesEqual().  At the moment there is no point
+		 * here and in ri_CompareWithCast().  At the moment there is no point
 		 * because cases involving nonidentical array types will be rejected
 		 * at constraint creation time.
 		 *
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 50c0247c300..b0d7f51be62 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,7 +338,7 @@ static char *pg_get_viewdef_worker(Oid viewoid,
 								   int prettyFlags, int wrapColumn);
 static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
 static int	decompile_column_index_array(Datum column_index_array, Oid relId,
-										 StringInfo buf);
+										 bool withPeriod, StringInfo buf);
 static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
 static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
 									const Oid *excludeOps,
@@ -2260,7 +2260,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				decompile_column_index_array(val, conForm->conrelid, &buf);
+				/* If it is a temporal foreign key then it uses PERIOD. */
+				decompile_column_index_array(val, conForm->conrelid, conForm->conperiod, &buf);
 
@@ -2271,7 +2272,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 
-				decompile_column_index_array(val, conForm->confrelid, &buf);
+				decompile_column_index_array(val, conForm->confrelid, conForm->conperiod, &buf);

@@ -2357,7 +2358,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (!isnull)
 				{
 
@@ -2392,7 +2393,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
 				if (conForm->conperiod)
 
@@ -2576,7 +2577,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  */
 static int
 decompile_column_index_array(Datum column_index_array, Oid relId,
-							 StringInfo buf)
+							 bool withPeriod, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2595,7 +2596,9 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
 		if (j == 0)
 			appendStringInfoString(buf, quote_identifier(colName));
 		else
-			appendStringInfo(buf, ", %s", quote_identifier(colName));
+			appendStringInfo(buf, ", %s%s",
+							 (withPeriod && j == nKeys - 1) ? "PERIOD " : "",
+							 quote_identifier(colName));
 	}
 
 	return nKeys;
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 96c00624b15..115217a6162 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -108,8 +108,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	bool		connoinherit;
 
 	/*
-	 * For primary keys and unique constraints, signifies the last column uses
-	 * overlaps instead of equals.
+	 * For primary keys, unique constraints, and foreign keys, signifies the
+	 * last column uses overlaps instead of equals.
 	 */
 	bool		conperiod;
 
@@ -127,20 +127,22 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	int16		confkey[1];
 
 	/*
-	 * If a foreign key, the OIDs of the PK = FK equality operators for each
-	 * column of the constraint
+	 * If a foreign key, the OIDs of the PK = FK equality/overlap operators
+	 * for each column of the constraint
 	 */
 	Oid			conpfeqop[1] BKI_LOOKUP(pg_operator);
 
 	/*
-	 * If a foreign key, the OIDs of the PK = PK equality operators for each
-	 * column of the constraint (i.e., equality for the referenced columns)
+	 * If a foreign key, the OIDs of the PK = PK equality/overlap operators
+	 * for each column of the constraint (i.e., equality for the referenced
+	 * columns)
 	 */
 	Oid			conppeqop[1] BKI_LOOKUP(pg_operator);
 
 	/*
-	 * If a foreign key, the OIDs of the FK = FK equality operators for each
-	 * column of the constraint (i.e., equality for the referencing columns)
+	 * If a foreign key, the OIDs of the FK = FK equality/overlap operators
+	 * for each column of the constraint (i.e., equality for the referencing
+	 * columns)
 	 */
 	Oid			conffeqop[1] BKI_LOOKUP(pg_operator);
 
@@ -277,6 +279,9 @@ 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);
 
 extern bool check_functional_grouping(Oid relid,
 									  Index varno, Index varlevelsup,
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 0c53d67d3ee..5fd095ea177 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -50,7 +50,7 @@ extern bool CheckIndexCompatible(Oid oldId,
 extern Oid	GetDefaultOpClass(Oid type_id, Oid am_id);
 extern Oid	ResolveOpClass(const List *opclass, Oid attrType,
 						   const char *accessMethodName, Oid accessMethodId);
-extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
+extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
 											 Oid *opid, StrategyNumber *strat);
 
 /* commands/functioncmds.c */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9dc4667d2bd..1cb7b02c5c2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2767,6 +2767,8 @@ typedef struct Constraint
 	RangeVar   *pktable;		/* Primary key table */
 	List	   *fk_attrs;		/* Attributes of foreign key */
 	List	   *pk_attrs;		/* Corresponding attrs in PK table */
+	bool		fk_with_period; /* Last attribute of FK uses PERIOD */
+	bool		pk_with_period; /* Last attribute of PK uses PERIOD */
 	char		fk_matchtype;	/* FULL, PARTIAL, SIMPLE */
 	char		fk_upd_action;	/* ON UPDATE action */
 	char		fk_del_action;	/* ON DELETE action */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f7fe834cf45..f9a4afd4723 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -339,6 +339,7 @@ PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 93b7c140158..290e77198ae 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -192,10 +192,10 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
 
 -- PK with two columns plus a multirange:
 CREATE TABLE temporal_mltrng2 (
-  id1 int4range,
-  id2 int4range,
-  valid_at datemultirange,
-  CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+	id1 int4range,
+	id2 int4range,
+	valid_at datemultirange,
+	CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
 );
 \d temporal_mltrng2
               Table "public.temporal_mltrng2"
@@ -1157,4 +1157,1085 @@ SELECT * FROM temporal3 ORDER BY id, valid_at;
 (1 row)
 
 DROP TABLE temporal3;
+--
+-- test FK dependencies
+--
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ERROR:  cannot drop column valid_at of table temporal3 because other objects depend on it
+DETAIL:  constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+--
+-- test FOREIGN KEY, range references range
+--
+-- test table setup
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (id int4range, valid_at daterange);
+ALTER TABLE temporal_rng
+  ADD CONSTRAINT temporal_rng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at int4range,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_rng2rng_fk2" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4range and daterange.
+-- works: PERIOD for both referenced and referencing
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_rng (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_rng2;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2rng
+           Table "public.temporal_fk2_rng2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | daterange |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_rng2rng;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2rng
+	ADD CONSTRAINT temporal_fk2_rng2rng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2rng
+           Table "public.temporal_fk2_rng2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | daterange |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk,
+	ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ERROR:  foreign key constraint "temporal_fk_rng2rng_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsrange and daterange.
+ALTER TABLE temporal_fk_rng2rng
+	ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_rng (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_rng2rng;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-02', '2018-02-03')),
+  ('[1,2)', daterange('2018-03-03', '2018-04-04')),
+  ('[2,3)', daterange('2018-01-01', '2018-01-05')),
+  ('[3,4)', daterange('2018-01-01', NULL));
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-01'), '[1,2)');
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_rng".
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK referencing inserts
+--
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-01'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_rng".
+-- now it should work:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+--
+-- test FK referencing updates
+--
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-03-01') WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-05-01') WHERE id = '[1,2)';
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-05-01)) is not present in table "temporal_rng".
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+ERROR:  insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-03-01)) is not present in table "temporal_rng".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES
+    ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+    ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+  INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+    ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+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".
+--
+-- test FK referenced updates NO ACTION
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_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 "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".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_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');
+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".
+-- 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');
+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".
+-- 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')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+--
+-- test FK referenced updates RESTRICT
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_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 "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".
+ROLLBACK;
+-- 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');
+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".
+-- 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')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+--
+-- test FK referenced deletes NO ACTION
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng 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".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+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".
+-- 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');
+--
+-- test FK referenced deletes RESTRICT
+--
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng 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".
+ROLLBACK;
+-- 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');
+--
+-- test ON UPDATE/DELETE options
+--
+-- test FK referenced updates CASCADE
+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)');
+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 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)');
+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
+-- test FK referenced updates SET DEFAULT
+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)');
+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
+--
+-- test FOREIGN KEY, multirange references multirange
+--
+-- test table setup
+DROP TABLE temporal_mltrng;
+CREATE TABLE temporal_mltrng ( id int4range, valid_at datemultirange);
+ALTER TABLE temporal_mltrng
+  ADD CONSTRAINT temporal_mltrng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- Can't create a FK with a mismatched multirange type
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at int4multirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_mltrng2mltrng_fk2" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4multirange and datemultirange.
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_mltrng2mltrng;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng
+);
+DROP TABLE temporal_fk_mltrng2mltrng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_mltrng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_mltrng (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_mltrng2;
+CREATE TABLE temporal_mltrng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at datemultirange,
+	CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_mltrng2mltrng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_mltrng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_mltrng2mltrng
+          Table "public.temporal_fk2_mltrng2mltrng"
+   Column   |      Type      | Collation | Nullable | Default 
+------------+----------------+-----------+----------+---------
+ id         | int4range      |           | not null | 
+ valid_at   | datemultirange |           | not null | 
+ parent_id1 | int4range      |           |          | 
+ parent_id2 | int4range      |           |          | 
+Indexes:
+    "temporal_fk2_mltrng2mltrng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_mltrng2mltrng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_mltrng2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_mltrng2mltrng;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk2_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_mltrng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_mltrng2mltrng
+          Table "public.temporal_fk2_mltrng2mltrng"
+   Column   |      Type      | Collation | Nullable | Default 
+------------+----------------+-----------+----------+---------
+ id         | int4range      |           | not null | 
+ valid_at   | datemultirange |           | not null | 
+ parent_id1 | int4range      |           |          | 
+ parent_id2 | int4range      |           |          | 
+Indexes:
+    "temporal_fk2_mltrng2mltrng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_mltrng2mltrng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_mltrng2(id1, id2, PERIOD valid_at)
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_mltrng (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03'))),
+  ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04'))),
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05'))),
+  ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,2)');
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+ERROR:  insert or update on table "temporal_fk_mltrng2mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), {[2018-01-02,2018-04-01)}) is not present in table "temporal_mltrng".
+-- okay again:
+DELETE FROM temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_mltrng2mltrng_fk';
+                                   pg_get_constraintdef                                   
+------------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_mltrng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK referencing inserts
+--
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+ERROR:  insert or update on table "temporal_fk_mltrng2mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), {[2018-01-02,2018-04-01)}) is not present in table "temporal_mltrng".
+-- now it should work:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-02-03', '2018-03-03')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+--
+-- test FK referencing updates
+--
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-03-01')) WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-05-01')) WHERE id = '[1,2)';
+ERROR:  insert or update on table "temporal_fk_mltrng2mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), {[2018-01-02,2018-05-01)}) is not present in table "temporal_mltrng".
+UPDATE temporal_fk_mltrng2mltrng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+ERROR:  insert or update on table "temporal_fk_mltrng2mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), {[2018-01-02,2018-03-01)}) is not present in table "temporal_mltrng".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES
+    ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+    ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+  INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+    ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_mltrng WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+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".
+--
+-- test FK referenced updates NO ACTION
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK update that fails because both are referenced:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+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".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+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".
+-- 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'));
+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".
+--
+-- test FK referenced updates RESTRICT
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+  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".
+ROLLBACK;
+-- 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'));
+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".
+--
+-- test FK referenced deletes NO ACTION
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_mltrng 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".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+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".
+--
+-- test FK referenced deletes RESTRICT
+--
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON DELETE RESTRICT;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_mltrng 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".
+ROLLBACK;
+--
+-- FK between partitioned tables
+--
+CREATE TABLE temporal_partitioned_rng (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_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)');
+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'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	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)');
+--
+-- partitioned FK referencing inserts
+--
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
+  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
+  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2010-01-01,2010-02-15)) is not present in table "temporal_partitioned_rng".
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([3,4), [2000-01-01,2000-02-15)) is not present in table "temporal_partitioned_rng".
+--
+-- partitioned FK referencing updates
+--
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,3)';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,5)' WHERE id = '[1,2)';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,2)' WHERE id = '[4,5)';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,2)';
+ERROR:  conflicting key value violates exclusion constraint "tfkp1_pkey"
+DETAIL:  Key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)) conflicts with existing key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)).
+--
+-- partitioned FK referenced updates NO ACTION
+--
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- 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_parent_id_valid_at_fkey" 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
+--
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+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_parent_id_valid_at_fkey" 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 RESTRICT
+--
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_partitioned_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- 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_parent_id_valid_at_fkey" 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 RESTRICT
+--
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+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_parent_id_valid_at_fkey" 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
+--
+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
+--
+-- partitioned FK referenced deletes CASCADE
+--
+--
+-- partitioned FK referenced updates SET NULL
+--
+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
+--
+-- partitioned FK referenced deletes SET NULL
+--
+--
+-- partitioned FK referenced updates SET DEFAULT
+--
+ALTER TABLE temporal_partitioned_fk_rng2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	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 DEFAULT ON UPDATE SET DEFAULT;
+ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
+--
+-- partitioned FK referenced deletes SET DEFAULT
+--
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
 RESET datestyle;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index aef130ee655..359613c0119 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -114,10 +114,10 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
 
 -- PK with two columns plus a multirange:
 CREATE TABLE temporal_mltrng2 (
-  id1 int4range,
-  id2 int4range,
-  valid_at datemultirange,
-  CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+	id1 int4range,
+	id2 int4range,
+	valid_at datemultirange,
+	CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
 );
 \d temporal_mltrng2
 SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_mltrng2_pk';
@@ -807,4 +807,1032 @@ SELECT * FROM temporal3 ORDER BY id, valid_at;
 
 DROP TABLE temporal3;
 
+--
+-- test FK dependencies
+--
+
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+	id int4range,
+	valid_at daterange,
+	CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+
+--
+-- test FOREIGN KEY, range references range
+--
+
+-- test table setup
+DROP TABLE temporal_rng;
+CREATE TABLE temporal_rng (id int4range, valid_at daterange);
+ALTER TABLE temporal_rng
+  ADD CONSTRAINT temporal_rng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at int4range,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+-- works: PERIOD for both referenced and referencing
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_rng2rng;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_rng
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_rng2rng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_rng
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_rng (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_rng2;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2rng
+DROP TABLE temporal_fk2_rng2rng;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2rng
+	ADD CONSTRAINT temporal_fk2_rng2rng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2rng
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk,
+	ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2rng;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-02', '2018-02-03')),
+  ('[1,2)', daterange('2018-03-03', '2018-04-04')),
+  ('[2,3)', daterange('2018-01-01', '2018-01-05')),
+  ('[3,4)', daterange('2018-01-01', NULL));
+
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-01'), '[1,2)');
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[1,2)', daterange('2018-01-02', '2018-02-01'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+-- now it should work:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', daterange('2018-02-03', '2018-03-03'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[2,3)', daterange('2018-01-02', '2018-04-01'), '[1,2)');
+
+--
+-- test FK referencing updates
+--
+
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-03-01') WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-05-01') WHERE id = '[1,2)';
+UPDATE temporal_fk_rng2rng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES
+    ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+    ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+  INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+    ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced:
+UPDATE temporal_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');
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_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');
+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');
+-- 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')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_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');
+ROLLBACK;
+-- 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');
+-- 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')
+WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- 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');
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_rng2rng;
+ALTER TABLE temporal_fk_rng2rng
+	DROP CONSTRAINT temporal_fk_rng2rng_fk;
+ALTER TABLE temporal_fk_rng2rng
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', daterange('2018-01-01', '2018-02-01')),
+  ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2rng
+    ALTER CONSTRAINT temporal_fk_rng2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+ROLLBACK;
+-- 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');
+
+--
+-- test ON UPDATE/DELETE options
+--
+
+-- test FK referenced updates CASCADE
+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)');
+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 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)');
+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;
+
+-- test FK referenced updates SET DEFAULT
+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)');
+ALTER TABLE temporal_fk_rng2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk_rng2rng_fk,
+	ADD CONSTRAINT temporal_fk_rng2rng_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng
+		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+
+--
+-- test FOREIGN KEY, multirange references multirange
+--
+
+-- test table setup
+DROP TABLE temporal_mltrng;
+CREATE TABLE temporal_mltrng ( id int4range, valid_at datemultirange);
+ALTER TABLE temporal_mltrng
+  ADD CONSTRAINT temporal_mltrng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched multirange type
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at int4multirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk2 FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+DROP TABLE temporal_fk_mltrng2mltrng;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_mltrng
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_mltrng
+);
+DROP TABLE temporal_fk_mltrng2mltrng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_mltrng
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_mltrng (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_mltrng2;
+CREATE TABLE temporal_mltrng2 (
+	id1 int4range,
+	id2 int4range,
+	valid_at datemultirange,
+	CONSTRAINT temporal_mltrng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_mltrng2mltrng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_mltrng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_mltrng2mltrng
+DROP TABLE temporal_fk2_mltrng2mltrng;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_mltrng2mltrng (
+	id int4range,
+	valid_at datemultirange,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	CONSTRAINT temporal_fk2_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE temporal_fk2_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk2_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_mltrng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_mltrng2mltrng
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_mltrng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_mltrng2mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03'))),
+  ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04'))),
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-01-05'))),
+  ('[3,4)', datemultirange(daterange('2018-01-01', NULL)));
+
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,2)');
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+-- okay again:
+DELETE FROM temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_mltrng2mltrng_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+-- now it should work:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2018-02-03', '2018-03-03')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[2,3)', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,2)');
+
+--
+-- test FK referencing updates
+--
+
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-03-01')) WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-05-01')) WHERE id = '[1,2)';
+UPDATE temporal_fk_mltrng2mltrng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_mltrng (id, valid_at) VALUES
+    ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+    ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+  INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+    ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_mltrng WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK update that fails because both are referenced:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+  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'));
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+ROLLBACK;
+-- 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'));
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at);
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+COMMIT;
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng (id, PERIOD valid_at)
+	ON DELETE RESTRICT;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+ROLLBACK;
+
+--
+-- FK between partitioned tables
+--
+
+CREATE TABLE temporal_partitioned_rng (
+	id int4range,
+	valid_at daterange,
+  name text,
+	CONSTRAINT temporal_paritioned_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)');
+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'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
+
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+	id int4range,
+	valid_at daterange,
+	parent_id int4range,
+	CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	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)');
+
+--
+-- partitioned FK referencing inserts
+--
+
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
+  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
+  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
+
+--
+-- partitioned FK referencing updates
+--
+
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,3)';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,5)' WHERE id = '[1,2)';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,2)' WHERE id = '[4,5)';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,2)';
+
+--
+-- partitioned FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- 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');
+
+--
+-- partitioned FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+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');
+
+--
+-- partitioned FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	DROP CONSTRAINT temporal_partitioned_fk_rng2rng_fk;
+ALTER TABLE temporal_partitioned_fk_rng2rng
+	ADD CONSTRAINT temporal_partitioned_fk_rng2rng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_partitioned_rng
+	ON DELETE RESTRICT;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- 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');
+
+--
+-- partitioned FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01'));
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+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');
+
+--
+-- partitioned FK referenced updates CASCADE
+--
+
+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;
+
+--
+-- partitioned FK referenced deletes CASCADE
+--
+
+--
+-- partitioned FK referenced updates SET NULL
+--
+
+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;
+
+--
+-- partitioned FK referenced deletes SET NULL
+--
+
+--
+-- partitioned FK referenced updates SET DEFAULT
+--
+
+ALTER TABLE temporal_partitioned_fk_rng2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	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 DEFAULT ON UPDATE SET DEFAULT;
+
+--
+-- partitioned FK referenced deletes SET DEFAULT
+--
+
+DROP TABLE temporal_partitioned_fk_rng2rng;
+DROP TABLE temporal_partitioned_rng;
+
 RESET datestyle;
-- 
2.42.0

From 1e05266d9e793eadd49b9e0562390c9353f62728 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v37 5/8] Add support funcs for FOR PORTION OF

- Adds intersect support procs.

  These just call the existing intersect functions,
  but they let us compute the portion of a row that is updated/deleted
  in a FOR PORTION command.

- Adds {multi,}range_without_portion support procs

  These return SETOF their input type and work like minus but don't
  fail on splits. They never contain empty elements. We will use this to
  compute FOR PORTION OF leftovers.
---
 contrib/bloom/blvalidate.c                    |   2 +-
 doc/src/sgml/catalogs.sgml                    |   2 +-
 doc/src/sgml/gist.sgml                        | 155 ++++++++++++++++-
 doc/src/sgml/xindex.sgml                      |  14 +-
 src/backend/access/brin/brin_validate.c       |   8 +-
 src/backend/access/gin/ginvalidate.c          |  12 +-
 src/backend/access/gist/gistvalidate.c        |  29 ++--
 src/backend/access/index/amvalidate.c         |   9 +-
 src/backend/access/nbtree/nbtvalidate.c       |   8 +-
 src/backend/access/spgist/spgvalidate.c       |   8 +-
 src/backend/utils/adt/multirangetypes.c       |  71 ++++++++
 src/backend/utils/adt/rangetypes.c            | 163 ++++++++++++++++++
 src/include/access/amvalidate.h               |   4 +-
 src/include/access/gist.h                     |   4 +-
 src/include/catalog/pg_amproc.dat             |  15 ++
 src/include/catalog/pg_proc.dat               |   8 +
 src/include/utils/rangetypes.h                |   2 +
 src/test/regress/expected/multirangetypes.out | 116 +++++++++++++
 src/test/regress/expected/rangetypes.out      |  54 ++++++
 src/test/regress/sql/multirangetypes.sql      |  22 +++
 src/test/regress/sql/rangetypes.sql           |  10 ++
 21 files changed, 677 insertions(+), 39 deletions(-)

diff --git a/contrib/bloom/blvalidate.c b/contrib/bloom/blvalidate.c
index 88c5a791975..9e39e39e174 100644
--- a/contrib/bloom/blvalidate.c
+++ b/contrib/bloom/blvalidate.c
@@ -105,7 +105,7 @@ blvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case BLOOM_HASH_PROC:
-				ok = check_amproc_signature(procform->amproc, INT4OID, false,
+				ok = check_amproc_signature(procform->amproc, INT4OID, false, false,
 											1, 1, opckeytype);
 				break;
 			case BLOOM_OPTIONS_PROC:
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index bfb97865e18..a5ff5c8da55 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -865,7 +865,7 @@
       </para>
       <para>
        The B-tree operator family this entry sorts according to, if an
-       ordering operator; zero if a search operator
+       ordering operator; zero if a search operator or portion operator
       </para></entry>
      </row>
     </tbody>
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 638d912dc2d..9e7a7f575b4 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -266,7 +266,7 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and seven that are optional.
+   <acronym>GiST</acronym> must provide, and nine that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -294,6 +294,12 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
    used by the operator class.  This lets the core code look up operators for
    temporal constraint indexes.
+   The optional thirteenth method <function>intersect</function> is used by
+   <literal>FOR PORTION OF</literal> to compute the new bounds of the updated/
+   deleted record.
+   The optional fourteenth method <function>without_portion</function> is used by
+   <literal>FOR PORTION OF</literal> to compute the leftover records outside the
+   targeted bounds.
  </para>
 
  <variablelist>
@@ -1235,6 +1241,153 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><function>intersect</function></term>
+     <listitem>
+      <para>
+       Given two values of this opclass, it returns their intersection.
+      </para>
+      <para>
+       This is used for temporal update commands to compute the
+       new bounds of the changed row.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this (using <literal>my_intersect</literal> as an example):
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_range_intersect(anyrange, anyrange)
+RETURNS anyrange
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this example:
+
+<programlisting>
+Datum
+my_range_intersect(PG_FUNCTION_ARGS)
+{
+    RangeType  *r1 = PG_GETARG_RANGE_P(0);
+    RangeType  *r2 = PG_GETARG_RANGE_P(1);
+    TypeCacheEntry *typcache;
+
+    /* Different types should be prevented by ANYRANGE matching rules */
+    if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2))                                                                  elog(ERROR, "range types do not match");
+
+    typcache = range_get_typcache(fcinfo, RangeTypeGetOid(r1));
+
+    PG_RETURN_RANGE_P(range_intersect_internal(typcache, r1, r2));
+}
+</programlisting>
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term><function>without_portion</function></term>
+     <listitem>
+      <para>
+       Given two values of this opclass, it subtracts the second for the first
+       and returns an array of the results.
+      </para>
+      <para>
+       This is used for temporal update/delete commands to compute the
+       bounds of the untouched duration.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this (using <literal>my_range_without_portion</literal> as an example):
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_range_without_portion(anyrange, anyrange)
+RETURNS anyarray
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this example:
+
+<programlisting>
+Datum
+my_range_without_portion(PG_FUNCTION_ARGS)
+{
+    typedef struct {
+        RangeType  *rs[2];
+        int         n;
+    } range_without_portion_fctx;
+
+    FuncCallContext *funcctx;
+    range_without_portion_fctx *fctx;
+    MemoryContext oldcontext;
+
+    /* stuff done only on the first call of the function */
+    if (SRF_IS_FIRSTCALL())
+    {
+        RangeType       *r1;
+        RangeType       *r2;
+        Oid              rngtypid;
+        TypeCacheEntry  *typcache;
+
+        /* create a function context for cross-call persistence */
+        funcctx = SRF_FIRSTCALL_INIT();
+
+        /*
+         * switch to memory context appropriate for multiple function calls
+         */
+        oldcontext = MemoryContextSwitchTo(funcctx-&gt;multi_call_memory_ctx);
+
+        r1 = PG_GETARG_RANGE_P(0);
+        r2 = PG_GETARG_RANGE_P(1);
+
+        /* Different types should be prevented by ANYRANGE matching rules */
+        if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2))
+            elog(ERROR, "range types do not match");
+
+        /* allocate memory for user context */
+        fctx = (range_without_portion_fctx *) palloc(sizeof(range_without_portion_fctx));
+
+        /*
+         * Initialize state.
+         * We can't store the range typcache in fn_extra because the caller
+         * uses that for the SRF state.
+         */
+        rngtypid = RangeTypeGetOid(r1);
+        typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+        if (typcache-&gt;rngelemtype == NULL)
+            elog(ERROR, "type %u is not a range type", rngtypid);
+        range_without_portion_internal(typcache, r1, r2, fctx-&gt;rs, &amp;fctx-&gt;n);
+
+        funcctx-&gt;user_fctx = fctx;
+        MemoryContextSwitchTo(oldcontext);
+    }
+
+    /* stuff done on every call of the function */
+    funcctx = SRF_PERCALL_SETUP();
+    fctx = funcctx-&gt;user_fctx;
+
+    if (funcctx-&gt;call_cntr &lt; fctx-&gt;n)
+    {
+        /*
+         * We must keep these on separate lines
+         * because SRF_RETURN_NEXT does call_cntr++:
+         */
+        RangeType *ret = fctx-&gt;rs[funcctx-&gt;call_cntr];
+        SRF_RETURN_NEXT(funcctx, RangeTypePGetDatum(ret));
+    }
+    else
+        /* do when there is no more left */
+        SRF_RETURN_DONE(funcctx);
+}
+</programlisting>
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15e0..f5966375ead 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
-   GiST indexes have twelve support functions, seven of which are optional,
+   GiST indexes have fourteen support functions, nine of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -596,6 +596,18 @@
         used by the operator class (optional)</entry>
        <entry>12</entry>
       </row>
+      <row>
+       <entry><function>intersect</function></entry>
+       <entry>computes intersection with <literal>FOR PORTION OF</literal>
+        bounds</entry>
+       <entry>13</entry>
+      </row>
+      <row>
+       <entry><function>without_portion</function></entry>
+       <entry>computes remaining duration(s) outside
+       <literal>FOR PORTION OF</literal> bounds</entry>
+       <entry>14</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c
index 539ac9cb064..082f96b847d 100644
--- a/src/backend/access/brin/brin_validate.c
+++ b/src/backend/access/brin/brin_validate.c
@@ -87,21 +87,21 @@ brinvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case BRIN_PROCNUM_OPCINFO:
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, true,
 											1, 1, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_ADDVALUE:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											4, 4, INTERNALOID, INTERNALOID,
 											INTERNALOID, INTERNALOID);
 				break;
 			case BRIN_PROCNUM_CONSISTENT:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											3, 4, INTERNALOID, INTERNALOID,
 											INTERNALOID, INT4OID);
 				break;
 			case BRIN_PROCNUM_UNION:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											3, 3, INTERNALOID, INTERNALOID,
 											INTERNALOID);
 				break;
diff --git a/src/backend/access/gin/ginvalidate.c b/src/backend/access/gin/ginvalidate.c
index 74420a129aa..748b94a893f 100644
--- a/src/backend/access/gin/ginvalidate.c
+++ b/src/backend/access/gin/ginvalidate.c
@@ -104,37 +104,37 @@ ginvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case GIN_COMPARE_PROC:
-				ok = check_amproc_signature(procform->amproc, INT4OID, false,
+				ok = check_amproc_signature(procform->amproc, INT4OID, false, false,
 											2, 2, opckeytype, opckeytype);
 				break;
 			case GIN_EXTRACTVALUE_PROC:
 				/* Some opclasses omit nullFlags */
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, false,
 											2, 3, opcintype, INTERNALOID,
 											INTERNALOID);
 				break;
 			case GIN_EXTRACTQUERY_PROC:
 				/* Some opclasses omit nullFlags and searchMode */
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, false,
 											5, 7, opcintype, INTERNALOID,
 											INT2OID, INTERNALOID, INTERNALOID,
 											INTERNALOID, INTERNALOID);
 				break;
 			case GIN_CONSISTENT_PROC:
 				/* Some opclasses omit queryKeys and nullFlags */
-				ok = check_amproc_signature(procform->amproc, BOOLOID, false,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, false,
 											6, 8, INTERNALOID, INT2OID,
 											opcintype, INT4OID,
 											INTERNALOID, INTERNALOID,
 											INTERNALOID, INTERNALOID);
 				break;
 			case GIN_COMPARE_PARTIAL_PROC:
-				ok = check_amproc_signature(procform->amproc, INT4OID, false,
+				ok = check_amproc_signature(procform->amproc, INT4OID, false, false,
 											4, 4, opckeytype, opckeytype,
 											INT2OID, INTERNALOID);
 				break;
 			case GIN_TRICONSISTENT_PROC:
-				ok = check_amproc_signature(procform->amproc, CHAROID, false,
+				ok = check_amproc_signature(procform->amproc, CHAROID, false, false,
 											7, 7, INTERNALOID, INT2OID,
 											opcintype, INT4OID,
 											INTERNALOID, INTERNALOID,
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 0901543a60a..0a2395fbaf6 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -106,36 +106,36 @@ gistvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case GIST_CONSISTENT_PROC:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, false,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, false,
 											5, 5, INTERNALOID, opcintype,
 											INT2OID, OIDOID, INTERNALOID);
 				break;
 			case GIST_UNION_PROC:
-				ok = check_amproc_signature(procform->amproc, opckeytype, false,
+				ok = check_amproc_signature(procform->amproc, opckeytype, false, false,
 											2, 2, INTERNALOID, INTERNALOID);
 				break;
 			case GIST_COMPRESS_PROC:
 			case GIST_DECOMPRESS_PROC:
 			case GIST_FETCH_PROC:
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, true,
 											1, 1, INTERNALOID);
 				break;
 			case GIST_PENALTY_PROC:
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, true,
 											3, 3, INTERNALOID,
 											INTERNALOID, INTERNALOID);
 				break;
 			case GIST_PICKSPLIT_PROC:
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, true,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, true,
 											2, 2, INTERNALOID, INTERNALOID);
 				break;
 			case GIST_EQUAL_PROC:
-				ok = check_amproc_signature(procform->amproc, INTERNALOID, false,
+				ok = check_amproc_signature(procform->amproc, INTERNALOID, false, false,
 											3, 3, opckeytype, opckeytype,
 											INTERNALOID);
 				break;
 			case GIST_DISTANCE_PROC:
-				ok = check_amproc_signature(procform->amproc, FLOAT8OID, false,
+				ok = check_amproc_signature(procform->amproc, FLOAT8OID, false, false,
 											5, 5, INTERNALOID, opcintype,
 											INT2OID, OIDOID, INTERNALOID);
 				break;
@@ -143,13 +143,21 @@ gistvalidate(Oid opclassoid)
 				ok = check_amoptsproc_signature(procform->amproc);
 				break;
 			case GIST_SORTSUPPORT_PROC:
-				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+				ok = check_amproc_signature(procform->amproc, VOIDOID, false, true,
 											1, 1, INTERNALOID);
 				break;
 			case GIST_STRATNUM_PROC:
-				ok = check_amproc_signature(procform->amproc, INT2OID, true,
+				ok = check_amproc_signature(procform->amproc, INT2OID, false, true,
 											1, 1, INT2OID);
 				break;
+			case GIST_INTERSECT_PROC:
+				ok = check_amproc_signature(procform->amproc, InvalidOid, false, true,
+											2, 2, opcintype, opcintype);
+				break;
+			case GIST_WITHOUT_PORTION_PROC:
+				ok = check_amproc_signature(procform->amproc, opcintype, true, true,
+											2, 2, opcintype, opcintype);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -271,7 +279,8 @@ gistvalidate(Oid opclassoid)
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
 			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
-			i == GIST_STRATNUM_PROC)
+			i == GIST_STRATNUM_PROC || i == GIST_INTERSECT_PROC ||
+			i == GIST_WITHOUT_PORTION_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/access/index/amvalidate.c b/src/backend/access/index/amvalidate.c
index 32bb477f328..b10308b9b66 100644
--- a/src/backend/access/index/amvalidate.c
+++ b/src/backend/access/index/amvalidate.c
@@ -149,7 +149,7 @@ identify_opfamily_groups(CatCList *oprlist, CatCList *proclist)
  * In any case the function result type must match restype exactly.
  */
 bool
-check_amproc_signature(Oid funcid, Oid restype, bool exact,
+check_amproc_signature(Oid funcid, Oid restype, bool retset, bool exact,
 					   int minargs, int maxargs,...)
 {
 	bool		result = true;
@@ -163,8 +163,9 @@ check_amproc_signature(Oid funcid, Oid restype, bool exact,
 		elog(ERROR, "cache lookup failed for function %u", funcid);
 	procform = (Form_pg_proc) GETSTRUCT(tp);
 
+		|| procform->proretset != retset || procform->pronargs < minargs
 
 	va_start(ap, maxargs);
@@ -191,7 +192,7 @@ check_amproc_signature(Oid funcid, Oid restype, bool exact,
 bool
 check_amoptsproc_signature(Oid funcid)
 {
-	return check_amproc_signature(funcid, VOIDOID, true, 1, 1, INTERNALOID);
+	return check_amproc_signature(funcid, VOIDOID, false, true, 1, 1, INTERNALOID);
 }
 
 /*
diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c
index e9d4cd60de3..04e43ef1dce 100644
--- a/src/backend/access/nbtree/nbtvalidate.c
+++ b/src/backend/access/nbtree/nbtvalidate.c
@@ -91,16 +91,16 @@ btvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case BTORDER_PROC:
-				ok = check_amproc_signature(procform->amproc, INT4OID, true,
+				ok = check_amproc_signature(procform->amproc, INT4OID, false, true,
 											2, 2, procform->amproclefttype,
 											procform->amprocrighttype);
 				break;
 			case BTSORTSUPPORT_PROC:
-				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+				ok = check_amproc_signature(procform->amproc, VOIDOID, false, true,
 											1, 1, INTERNALOID);
 				break;
 			case BTINRANGE_PROC:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											5, 5,
 											procform->amproclefttype,
 											procform->amproclefttype,
@@ -108,7 +108,7 @@ btvalidate(Oid opclassoid)
 											BOOLOID, BOOLOID);
 				break;
 			case BTEQUALIMAGE_PROC:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											1, 1, OIDOID);
 				break;
 			case BTOPTIONS_PROC:
diff --git a/src/backend/access/spgist/spgvalidate.c b/src/backend/access/spgist/spgvalidate.c
index 850c3b675d6..f7a397cb8cf 100644
--- a/src/backend/access/spgist/spgvalidate.c
+++ b/src/backend/access/spgist/spgvalidate.c
@@ -109,7 +109,7 @@ spgvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case SPGIST_CONFIG_PROC:
-				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+				ok = check_amproc_signature(procform->amproc, VOIDOID, false, true,
 											2, 2, INTERNALOID, INTERNALOID);
 				configIn.attType = procform->amproclefttype;
 				memset(&configOut, 0, sizeof(configOut));
@@ -164,11 +164,11 @@ spgvalidate(Oid opclassoid)
 			case SPGIST_CHOOSE_PROC:
 			case SPGIST_PICKSPLIT_PROC:
 			case SPGIST_INNER_CONSISTENT_PROC:
-				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
+				ok = check_amproc_signature(procform->amproc, VOIDOID, false, true,
 											2, 2, INTERNALOID, INTERNALOID);
 				break;
 			case SPGIST_LEAF_CONSISTENT_PROC:
-				ok = check_amproc_signature(procform->amproc, BOOLOID, true,
+				ok = check_amproc_signature(procform->amproc, BOOLOID, false, true,
 											2, 2, INTERNALOID, INTERNALOID);
 				break;
 			case SPGIST_COMPRESS_PROC:
@@ -177,7 +177,7 @@ spgvalidate(Oid opclassoid)
 					ok = false;
 				else
 					ok = check_amproc_signature(procform->amproc,
-												configOutLeafType, true,
+												configOutLeafType, false, true,
 												1, 1, procform->amproclefttype);
 				break;
 			case SPGIST_OPTIONS_PROC:
diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index 558c6c18c34..8f722b6722d 100644
--- a/src/backend/utils/adt/multirangetypes.c
+++ b/src/backend/utils/adt/multirangetypes.c
@@ -1225,6 +1225,77 @@ multirange_minus_internal(Oid mltrngtypoid, TypeCacheEntry *rangetyp,
 	return make_multirange(mltrngtypoid, rangetyp, range_count3, ranges3);
 }
 
+/*
+ * multirange minus but returning the result as a SRF,
+ * with no rows if the result would be empty.
+ */
+Datum
+multirange_without_portion(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	MemoryContext oldcontext;
+
+	if (!SRF_IS_FIRSTCALL())
+	{
+		/* We never have more than one result */
+		funcctx = SRF_PERCALL_SETUP();
+		SRF_RETURN_DONE(funcctx);
+	}
+	else
+	{
+		MultirangeType *mr1;
+		MultirangeType *mr2;
+		Oid			mltrngtypoid;
+		TypeCacheEntry *typcache;
+		TypeCacheEntry *rangetyp;
+		int32		range_count1;
+		int32		range_count2;
+		RangeType **ranges1;
+		RangeType **ranges2;
+		MultirangeType *mr;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		/* get args, detoasting into multi-call memory context */
+		mr1 = PG_GETARG_MULTIRANGE_P(0);
+		mr2 = PG_GETARG_MULTIRANGE_P(1);
+
+		mltrngtypoid = MultirangeTypeGetOid(mr1);
+		typcache = lookup_type_cache(mltrngtypoid, TYPECACHE_MULTIRANGE_INFO);
+		if (typcache->rngtype == NULL)
+			elog(ERROR, "type %u is not a multirange type", mltrngtypoid);
+		rangetyp = typcache->rngtype;
+
+		if (MultirangeIsEmpty(mr1) || MultirangeIsEmpty(mr2))
+			mr = mr1;
+		else
+		{
+			multirange_deserialize(rangetyp, mr1, &range_count1, &ranges1);
+			multirange_deserialize(rangetyp, mr2, &range_count2, &ranges2);
+
+			mr = multirange_minus_internal(mltrngtypoid,
+										   rangetyp,
+										   range_count1,
+										   ranges1,
+										   range_count2,
+										   ranges2);
+		}
+
+		MemoryContextSwitchTo(oldcontext);
+
+		funcctx = SRF_PERCALL_SETUP();
+		if (MultirangeIsEmpty(mr))
+			SRF_RETURN_DONE(funcctx);
+		else
+			SRF_RETURN_NEXT(funcctx, MultirangeTypePGetDatum(mr));
+	}
+}
+
 /* multirange intersection */
 Datum
 multirange_intersect(PG_FUNCTION_ARGS)
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 2d94a6b8774..2ab27bb7573 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -31,6 +31,7 @@
 #include "postgres.h"
 
 #include "common/hashfn.h"
+#include "funcapi.h"
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -39,6 +40,7 @@
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/optimizer.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/date.h"
 #include "utils/lsyscache.h"
@@ -1213,6 +1215,167 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 	return false;
 }
 
+/* subtraction but returning an array to accommodate splits */
+Datum
+range_without_portion(PG_FUNCTION_ARGS)
+{
+	typedef struct {
+		RangeType  *rs[2];
+		int			n;
+	} range_without_portion_fctx;
+
+	FuncCallContext *funcctx;
+	range_without_portion_fctx *fctx;
+	MemoryContext oldcontext;
+
+	/* stuff done only on the first call of the function */
+	if (SRF_IS_FIRSTCALL())
+	{
+		RangeType	   *r1;
+		RangeType	   *r2;
+		Oid				rngtypid;
+		TypeCacheEntry *typcache;
+
+		/* create a function context for cross-call persistence */
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/*
+		 * switch to memory context appropriate for multiple function calls
+		 */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		r1 = PG_GETARG_RANGE_P(0);
+		r2 = PG_GETARG_RANGE_P(1);
+
+		/* Different types should be prevented by ANYRANGE matching rules */
+		if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2))
+			elog(ERROR, "range types do not match");
+
+		/* allocate memory for user context */
+		fctx = (range_without_portion_fctx *) palloc(sizeof(range_without_portion_fctx));
+
+		/*
+		 * Initialize state.
+		 * We can't store the range typcache in fn_extra because the caller
+		 * uses that for the SRF state.
+		 */
+		rngtypid = RangeTypeGetOid(r1);
+		typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
+		if (typcache->rngelemtype == NULL)
+			elog(ERROR, "type %u is not a range type", rngtypid);
+		range_without_portion_internal(typcache, r1, r2, fctx->rs, &fctx->n);
+
+		funcctx->user_fctx = fctx;
+		MemoryContextSwitchTo(oldcontext);
+	}
+
+	/* stuff done on every call of the function */
+	funcctx = SRF_PERCALL_SETUP();
+	fctx = funcctx->user_fctx;
+
+	if (funcctx->call_cntr < fctx->n)
+	{
+		/*
+		 * We must keep these on separate lines
+		 * because SRF_RETURN_NEXT does call_cntr++:
+		 */
+		RangeType *ret = fctx->rs[funcctx->call_cntr];
+		SRF_RETURN_NEXT(funcctx, RangeTypePGetDatum(ret));
+	}
+	else
+		/* do when there is no more left */
+		SRF_RETURN_DONE(funcctx);
+}
+
+/*
+ * range_without_portion_internal - Sets outputs and outputn to the ranges
+ * remaining and their count (respectively) after subtracting r2 from r1.
+ * The array should never contain empty ranges.
+ * The outputs will be ordered. We expect that outputs is an array of
+ * RangeType pointers, already allocated with two slots.
+ */
+void
+range_without_portion_internal(TypeCacheEntry *typcache, RangeType *r1,
+							   RangeType *r2, RangeType **outputs, int *outputn)
+{
+	int			cmp_l1l2,
+				cmp_l1u2,
+				cmp_u1l2,
+				cmp_u1u2;
+	RangeBound	lower1,
+				lower2;
+	RangeBound	upper1,
+				upper2;
+	bool		empty1,
+				empty2;
+
+	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+	if (empty1)
+	{
+		/* if r1 is empty then r1 - r2 is empty, so return zero results */
+		*outputn = 0;
+		return;
+	}
+	else if (empty2)
+	{
+		/* r2 is empty so the result is just r1 (which we know is not empty) */
+		outputs[0] = r1;
+		*outputn = 1;
+		return;
+	}
+
+	/*
+	 * Use the same logic as range_minus_internal,
+	 * but support the split case
+	 */
+	cmp_l1l2 = range_cmp_bounds(typcache, &lower1, &lower2);
+	cmp_l1u2 = range_cmp_bounds(typcache, &lower1, &upper2);
+	cmp_u1l2 = range_cmp_bounds(typcache, &upper1, &lower2);
+	cmp_u1u2 = range_cmp_bounds(typcache, &upper1, &upper2);
+
+	if (cmp_l1l2 < 0 && cmp_u1u2 > 0)
+	{
+		lower2.inclusive = !lower2.inclusive;
+		lower2.lower = false;	/* it will become the upper bound */
+		outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL);
+
+		upper2.inclusive = !upper2.inclusive;
+		upper2.lower = true;	/* it will become the lower bound */
+		outputs[1] = make_range(typcache, &upper2, &upper1, false, NULL);
+
+		*outputn = 2;
+	}
+	else if (cmp_l1u2 > 0 || cmp_u1l2 < 0)
+	{
+		outputs[0] = r1;
+		*outputn = 1;
+	}
+	else if (cmp_l1l2 >= 0 && cmp_u1u2 <= 0)
+	{
+		*outputn = 0;
+	}
+	else if (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0)
+	{
+		lower2.inclusive = !lower2.inclusive;
+		lower2.lower = false;	/* it will become the upper bound */
+		outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL);
+		*outputn = 1;
+	}
+	else if (cmp_l1l2 >= 0 && cmp_u1u2 >= 0 && cmp_l1u2 <= 0)
+	{
+		upper2.inclusive = !upper2.inclusive;
+		upper2.lower = true;	/* it will become the lower bound */
+		outputs[0] = make_range(typcache, &upper2, &upper1, false, NULL);
+		*outputn = 1;
+	}
+	else
+	{
+		elog(ERROR, "unexpected case in range_without_portion");
+	}
+}
+
 /* range -> range aggregate functions */
 
 Datum
diff --git a/src/include/access/amvalidate.h b/src/include/access/amvalidate.h
index 424ab63fa5a..61039542dac 100644
--- a/src/include/access/amvalidate.h
+++ b/src/include/access/amvalidate.h
@@ -28,8 +28,8 @@ typedef struct OpFamilyOpFuncGroup
 
 /* Functions in access/index/amvalidate.c */
 extern List *identify_opfamily_groups(CatCList *oprlist, CatCList *proclist);
-extern bool check_amproc_signature(Oid funcid, Oid restype, bool exact,
-								   int minargs, int maxargs,...);
+extern bool check_amproc_signature(Oid funcid, Oid restype, bool retset,
+								   bool exact, int minargs, int maxargs,...);
 extern bool check_amoptsproc_signature(Oid funcid);
 extern bool check_amop_signature(Oid opno, Oid restype,
 								 Oid lefttype, Oid righttype);
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index 22dd04c1418..1635ab00ad2 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -40,7 +40,9 @@
 #define GIST_OPTIONS_PROC				10
 #define GIST_SORTSUPPORT_PROC			11
 #define GIST_STRATNUM_PROC				12
-#define GISTNProcs					12
+#define GIST_INTERSECT_PROC				13
+#define GIST_WITHOUT_PORTION_PROC		14
+#define GISTNProcs					14
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 352558c1f06..f0ad84561cb 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -510,6 +510,9 @@
 { amprocfamily => 'gist/box_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/box_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '13',
+  amproc => 'box_intersect(box,box)' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '1',
   amproc => 'gist_poly_consistent' },
@@ -610,6 +613,12 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '13',
+  amproc => 'range_intersect(anyrange,anyrange)' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '14',
+  amproc => 'range_without_portion(anyrange,anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -650,6 +659,12 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '13',
+  amproc => 'multirange_intersect(anymultirange,anymultirange)' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '14',
+  amproc => 'multirange_without_portion(anymultirange,anymultirange)' },
 
 # gin
 { amprocfamily => 'gin/array_ops', amproclefttype => 'anyarray',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cee40c1e4db..34674686ad3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10604,6 +10604,10 @@
 { oid => '3869',
   proname => 'range_minus', prorettype => 'anyrange',
   proargtypes => 'anyrange anyrange', prosrc => 'range_minus' },
+{ oid => '8408', descr => 'remove portion from range',
+  proname => 'range_without_portion', prorows => '2',
+  proretset => 't', prorettype => 'anyrange',
+  proargtypes => 'anyrange anyrange', prosrc => 'range_without_portion' },
 { oid => '3870', descr => 'less-equal-greater',
   proname => 'range_cmp', prorettype => 'int4',
   proargtypes => 'anyrange anyrange', prosrc => 'range_cmp' },
@@ -10891,6 +10895,10 @@
 { oid => '4271',
   proname => 'multirange_minus', prorettype => 'anymultirange',
   proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_minus' },
+{ oid => '8406', descr => 'remove portion from multirange',
+  proname => 'multirange_without_portion', prorows => '1',
+  proretset => 't', prorettype => 'anymultirange',
+  proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_without_portion' },
 { oid => '4272',
   proname => 'multirange_intersect', prorettype => 'anymultirange',
   proargtypes => 'anymultirange anymultirange',
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 2b574873cef..e1b256b65f4 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,5 +164,7 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache);
 extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1,
 								 const RangeType *r2, RangeType **output1,
 								 RangeType **output2);
+extern void range_without_portion_internal(TypeCacheEntry *typcache, RangeType *r1,
+									RangeType *r2, RangeType **outputs, int *outputn);
 
 #endif							/* RANGETYPES_H */
diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out
index c6363ebeb24..11aa282ff35 100644
--- a/src/test/regress/expected/multirangetypes.out
+++ b/src/test/regress/expected/multirangetypes.out
@@ -2200,6 +2200,122 @@ SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0
  {[1,2),[4,5)}
 (1 row)
 
+-- without_portion
+SELECT multirange_without_portion(nummultirange(), nummultirange());
+ multirange_without_portion 
+----------------------------
+(0 rows)
+
+SELECT multirange_without_portion(nummultirange(), nummultirange(numrange(1,2)));
+ multirange_without_portion 
+----------------------------
+(0 rows)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange());
+ multirange_without_portion 
+----------------------------
+ {[1,2)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(3,4)), nummultirange());
+ multirange_without_portion 
+----------------------------
+ {[1,2),[3,4)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(1,2)));
+ multirange_without_portion 
+----------------------------
+(0 rows)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(2,4)));
+ multirange_without_portion 
+----------------------------
+ {[1,2)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(3,4)));
+ multirange_without_portion 
+----------------------------
+ {[1,2)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(1,2)));
+ multirange_without_portion 
+----------------------------
+ {[2,4)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(2,3)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[3,4)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(0,8)));
+ multirange_without_portion 
+----------------------------
+(0 rows)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(0,2)));
+ multirange_without_portion 
+----------------------------
+ {[2,4)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,8)), nummultirange(numrange(0,2), numrange(3,4)));
+ multirange_without_portion 
+----------------------------
+ {[2,3),[4,8)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,8)), nummultirange(numrange(2,3), numrange(5,null)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[3,5)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[4,5)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(2,4)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[4,5)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(3,5)));
+ multirange_without_portion 
+----------------------------
+ {[1,2)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(0,9)));
+ multirange_without_portion 
+----------------------------
+(0 rows)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,3), numrange(4,5)), nummultirange(numrange(2,9)));
+ multirange_without_portion 
+----------------------------
+ {[1,2)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(8,9)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[4,5)}
+(1 row)
+
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0), numrange(8,9)));
+ multirange_without_portion 
+----------------------------
+ {[1,2),[4,5)}
+(1 row)
+
 -- intersection
 SELECT nummultirange() * nummultirange();
  ?column? 
diff --git a/src/test/regress/expected/rangetypes.out b/src/test/regress/expected/rangetypes.out
index a7cc220bf0d..ab2309e8c1d 100644
--- a/src/test/regress/expected/rangetypes.out
+++ b/src/test/regress/expected/rangetypes.out
@@ -481,6 +481,60 @@ select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
  empty
 (1 row)
 
+select range_without_portion('empty'::numrange, numrange(2.0, 3.0));
+ range_without_portion 
+-----------------------
+(0 rows)
+
+select range_without_portion(numrange(1.1, 2.2), 'empty'::numrange);
+ range_without_portion 
+-----------------------
+ [1.1,2.2)
+(1 row)
+
+select range_without_portion(numrange(1.1, 2.2), numrange(2.0, 3.0));
+ range_without_portion 
+-----------------------
+ [1.1,2.0)
+(1 row)
+
+select range_without_portion(numrange(1.1, 2.2), numrange(2.2, 3.0));
+ range_without_portion 
+-----------------------
+ [1.1,2.2)
+(1 row)
+
+select range_without_portion(numrange(1.1, 2.2,'[]'), numrange(2.0, 3.0));
+ range_without_portion 
+-----------------------
+ [1.1,2.0)
+(1 row)
+
+select range_without_portion(numrange(1.0, 3.0), numrange(1.5, 2.0));
+ range_without_portion 
+-----------------------
+ [1.0,1.5)
+ [2.0,3.0)
+(2 rows)
+
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
+ range_without_portion 
+-----------------------
+ [10.1,12.2]
+(1 row)
+
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
+ range_without_portion 
+-----------------------
+(0 rows)
+
+select range_without_portion(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]'));
+ range_without_portion 
+-----------------------
+ [1.0,1.5]
+ (2.0,3.0]
+(2 rows)
+
 select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5);
  ?column? 
 ----------
diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql
index 41d5524285a..0bfa71caca0 100644
--- a/src/test/regress/sql/multirangetypes.sql
+++ b/src/test/regress/sql/multirangetypes.sql
@@ -414,6 +414,28 @@ SELECT nummultirange(numrange(1,3), numrange(4,5)) - nummultirange(numrange(2,9)
 SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(8,9));
 SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0), numrange(8,9));
 
+-- without_portion
+SELECT multirange_without_portion(nummultirange(), nummultirange());
+SELECT multirange_without_portion(nummultirange(), nummultirange(numrange(1,2)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange());
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(3,4)), nummultirange());
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(1,2)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(2,4)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2)), nummultirange(numrange(3,4)));
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(1,2)));
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(2,3)));
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(0,8)));
+SELECT multirange_without_portion(nummultirange(numrange(1,4)), nummultirange(numrange(0,2)));
+SELECT multirange_without_portion(nummultirange(numrange(1,8)), nummultirange(numrange(0,2), numrange(3,4)));
+SELECT multirange_without_portion(nummultirange(numrange(1,8)), nummultirange(numrange(2,3), numrange(5,null)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(2,4)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(3,5)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(0,9)));
+SELECT multirange_without_portion(nummultirange(numrange(1,3), numrange(4,5)), nummultirange(numrange(2,9)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(8,9)));
+SELECT multirange_without_portion(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0), numrange(8,9)));
+
 -- intersection
 SELECT nummultirange() * nummultirange();
 SELECT nummultirange() * nummultirange(numrange(1,2));
diff --git a/src/test/regress/sql/rangetypes.sql b/src/test/regress/sql/rangetypes.sql
index a5ecdf5372f..7fc805d9ffa 100644
--- a/src/test/regress/sql/rangetypes.sql
+++ b/src/test/regress/sql/rangetypes.sql
@@ -107,6 +107,16 @@ select numrange(1.1, 2.2,'[]') - numrange(2.0, 3.0);
 select range_minus(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
 select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
 
+select range_without_portion('empty'::numrange, numrange(2.0, 3.0));
+select range_without_portion(numrange(1.1, 2.2), 'empty'::numrange);
+select range_without_portion(numrange(1.1, 2.2), numrange(2.0, 3.0));
+select range_without_portion(numrange(1.1, 2.2), numrange(2.2, 3.0));
+select range_without_portion(numrange(1.1, 2.2,'[]'), numrange(2.0, 3.0));
+select range_without_portion(numrange(1.0, 3.0), numrange(1.5, 2.0));
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]'));
+select range_without_portion(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]'));
+select range_without_portion(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]'));
+
 select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5);
 select numrange(1.0, 2.0) << numrange(3.0, 4.0);
 select numrange(1.0, 3.0,'[]') << numrange(3.0, 4.0,'[]');
-- 
2.42.0

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

- Added bison support for FOR PORTION OF syntax. The bounds must be
  constant, so we forbid column references, subqueries, etc. But we
  permit the magic UNBOUNDED keyword as the FROM or the TO (or both)
  to perform an unbounded update/delete. We also accept functions like
  NOW().
- Added logic to executor to insert new rows for the "leftover" part of
  a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
  trigger functions. Our triggers use this to learn what bounds were
  given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   5 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   4 +
 contrib/sepgsql/proc.c                        |   2 +-
 contrib/unaccent/unaccent.c                   |   2 +-
 doc/src/sgml/catalogs.sgml                    |   4 +-
 doc/src/sgml/ref/delete.sgml                  |  69 ++
 doc/src/sgml/ref/update.sgml                  |  87 +++
 doc/src/sgml/trigger.sgml                     |  60 +-
 src/backend/access/gist/gistvalidate.c        |  34 +-
 src/backend/commands/explain.c                |   2 +-
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/commands/trigger.c                |  49 ++
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/nodeModifyTable.c        | 300 +++++++-
 src/backend/nodes/nodeFuncs.c                 |  24 +
 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                  | 242 ++++++-
 src/backend/parser/gram.y                     |  56 +-
 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          |  40 +
 src/backend/utils/adt/ri_triggers.c           |  74 ++
 src/backend/utils/cache/lsyscache.c           |  57 +-
 src/include/access/stratnum.h                 |   2 +-
 src/include/commands/trigger.h                |   1 +
 src/include/nodes/execnodes.h                 |  26 +
 src/include/nodes/parsenodes.h                |  46 +-
 src/include/nodes/pathnodes.h                 |   1 +
 src/include/nodes/plannodes.h                 |   1 +
 src/include/nodes/primnodes.h                 |  26 +
 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/include/utils/lsyscache.h                 |   4 +-
 src/test/regress/expected/for_portion_of.out  | 685 ++++++++++++++++++
 src/test/regress/expected/opr_sanity.out      |   1 +
 src/test/regress/expected/privileges.out      |  18 +
 src/test/regress/expected/updatable_views.out |  32 +
 .../regress/expected/without_overlaps.out     | 140 ++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/for_portion_of.sql       | 531 ++++++++++++++
 src/test/regress/sql/opr_sanity.sql           |   1 +
 src/test/regress/sql/privileges.sql           |  18 +
 src/test/regress/sql/updatable_views.sql      |  14 +
 src/test/regress/sql/without_overlaps.sql     |  88 +++
 51 files changed, 2727 insertions(+), 64 deletions(-)
 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 39b2b317e87..01dae7c2919 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -6362,6 +6362,11 @@ DELETE FROM ft2
 
index 8be9f99c192..d58c27d2ebb 100644
@@ -1571,6 +1571,10 @@ DELETE FROM ft2
 
+-- Test UPDATE/DELETE with FOR PORTION OF
+UPDATE ft2 FOR PORTION OF c3 FROM 'a' TO 'b' SET c1 = 1;
+DELETE FROM ft2 FOR PORTION OF c3 FROM 'a' TO 'b';
+
 -- Test that trigger on remote table works as expected
 CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
 BEGIN
diff --git a/contrib/sepgsql/proc.c b/contrib/sepgsql/proc.c
index 4899bc2a3e3..20120fd531f 100644
--- a/contrib/sepgsql/proc.c
+++ b/contrib/sepgsql/proc.c
@@ -161,7 +161,7 @@ sepgsql_proc_drop(Oid functionId)
 	 * check db_schema:{remove_name} permission
 	 */
 	object.classId = NamespaceRelationId;
-	object.objectId = get_func_namespace(functionId);
+	object.objectId = get_func_namespace(functionId, true);
 	object.objectSubId = 0;
 	audit_name = getObjectIdentity(&object, false);
 
diff --git a/contrib/unaccent/unaccent.c b/contrib/unaccent/unaccent.c
index 707962305f8..3b7646ec72a 100644
--- a/contrib/unaccent/unaccent.c
+++ b/contrib/unaccent/unaccent.c
@@ -449,7 +449,7 @@ unaccent_dict(PG_FUNCTION_ARGS)
 		 * Use the "unaccent" dictionary that is in the same schema that this
 		 * function is in.
 		 */
-		Oid			procnspid = get_func_namespace(fcinfo->flinfo->fn_oid);
+		Oid			procnspid = get_func_namespace(fcinfo->flinfo->fn_oid, true);
 		const char *dictname = "unaccent";
 
 		dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a5ff5c8da55..84a2f7e4106 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -833,7 +833,7 @@
        <structfield>amoppurpose</structfield> <type>char</type>
       </para>
       <para>
-       Operator purpose, either <literal>s</literal> for search or
+       Operator purpose, either <literal>s</literal> for search, or
        <literal>o</literal> for ordering
       </para></entry>
      </row>
@@ -865,7 +865,7 @@
       </para>
       <para>
        The B-tree operator family this entry sorts according to, if an
-       ordering operator; zero if a search operator or portion operator
+       ordering operator; zero if a search operator
       </para></entry>
      </row>
     </tbody>
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 0b6fa005123..0355b6085f1 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -23,6 +23,7 @@ PostgreSQL documentation
 <synopsis>
 [ WITH [ RECURSIVE ] <replaceable class="parameter">with_query</replaceable> [, ...] ]
 DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">alias</replaceable> ]
+    [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</replaceable> ]
     [ USING <replaceable class="parameter">from_item</replaceable> [, ...] ]
     [ WHERE <replaceable class="parameter">condition</replaceable> | WHERE CURRENT OF <replaceable class="parameter">cursor_name</replaceable> ]
     [ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
@@ -54,6 +55,39 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
 
+   <varlistentry>
index babb34fa511..ed8c5748a26 100644
 
+  <para>
 
+   <varlistentry>
index 31626536a2e..f3e66da014a 100644
@@ -546,17 +546,18 @@ CALLED_AS_TRIGGER(fcinfo)
 
@@ -824,6 +825,43 @@ typedef struct Trigger
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry>
 
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 0a2395fbaf6..4eb6ff42836 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -201,7 +201,7 @@ gistvalidate(Oid opclassoid)
 		}
 
@@ -228,24 +228,38 @@ gistvalidate(Oid opclassoid)
 
 	/* Now check for inconsistent groups of operators/functions */
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5771aabf40a..0487b07746d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4168,7 +4168,7 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 
 						objectname = get_func_name(funcid);
 						if (es->verbose)
-							namespace = get_namespace_name_or_temp(get_func_namespace(funcid));
+							namespace = get_namespace_name_or_temp(get_func_namespace(funcid, true));
 					}
 				}
 				objecttag = "Function Name";
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b1716cdbce1..8da83125262 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12324,6 +12324,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 29d30bfb6f7..3578fb0a4d4 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -47,6 +47,7 @@
 #include "storage/lmgr.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/datum.h"
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
 #include "utils/inval.h"
@@ -2632,6 +2633,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];
@@ -2731,6 +2733,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;
@@ -2822,6 +2825,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -2885,6 +2889,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];
@@ -3020,6 +3025,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++)
@@ -3169,6 +3175,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -3635,6 +3642,7 @@ typedef struct AfterTriggerSharedData
@@ -3908,6 +3916,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);

@@ -4117,6 +4126,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
@@ -4485,6 +4495,7 @@ AfterTriggerExecute(EState *estate,

@@ -6019,6 +6030,43 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6434,6 +6482,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 
index 4d7c92d63c1..cf239156da9 100644
@@ -1248,6 +1248,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index 4913e493199..a0f29285f45 100644
@@ -65,8 +65,10 @@
 
@@ -144,6 +146,10 @@ static bool ExecOnConflictUpdate(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,
@@ -166,6 +172,7 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
 static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
 										   ResultRelInfo *resultRelInfo,
 										   bool canSetTag);
+static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate);

 /*
@@ -1211,6 +1218,169 @@ 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;
+	bool isNull = false;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+	TupleTableSlot *leftoverSlot = fpoState->fp_Leftover;
+	TupleConversionMap *map = NULL;
+	HeapTuple oldtuple = NULL;
+	CmdType	oldOperation;
+	TransitionCaptureState *oldTcs;
+	FmgrInfo flinfo;
+	ReturnSetInfo rsi;
+	bool didInit = false;
+	bool shouldFree = false;
+	LOCAL_FCINFO(fcinfo, 2);
+
+	/*
+	 * Get the range of the old pre-UPDATE/DELETE tuple,
+	 * so we can intersect it with the FOR PORTION OF target
+	 * and see if there are any "leftovers" to insert.
+	 *
+	 * We have already locked the tuple in ExecUpdate/ExecDelete
+	 * and it has passed EvalPlanQual.
+	 * Make sure we're looking at the most recent version.
+	 * Otherwise concurrent updates of the same tuple in READ COMMITTED
+	 * could insert conflicting "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.
+	 * 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];
+	oldRange = slot_getattr(oldtupleSlot, rangeAttno, &isNull);
+
+	if (isNull)
+		elog(ERROR, "found a NULL range in a temporal table");
+
+	/*
+	 * Get the range's type cache entry. This is worth caching for the whole UPDATE
+	 * 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 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;
+
+	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");
+
+		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 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. */
+			// TODO: Do we need a more systematic way of doing this,
+			// e.g. a new mtstate or even a separate ForPortionOfLeftovers node?
+			oldOperation = mtstate->operation;
+			mtstate->operation = CMD_INSERT;
+			oldTcs = mtstate->mt_transition_capture;
+
+			didInit = true;
+		}
+
+		/* store the new range */
+		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;
+
+		ExecSetupTransitionCaptureState(mtstate, estate);
+		ExecInsert(context, resultRelInfo, leftoverSlot, node->canSetTag, NULL, NULL);
+	}
+
+	if (didInit)
+	{
+		mtstate->operation = oldOperation;
+		mtstate->mt_transition_capture = oldTcs;
+
+		if (shouldFree)
+			heap_freetuple(oldtuple);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1363,7 +1533,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 leftovers from a FOR PORTION OF
+ * delete.
  */
 static void
 ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1396,6 +1567,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
@@ -1776,7 +1952,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2142,6 +2322,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4573,6 +4758,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index d2e2af4f811..8c56dc67c30 100644
@@ -2561,6 +2561,14 @@ expression_tree_walker_impl(Node *node,
@@ -2707,6 +2715,8 @@ query_tree_walker_impl(Query *query,
 	if (WALK(query->mergeJoinCondition))
@@ -3586,6 +3596,19 @@ expression_tree_mutator_impl(Node *node,
@@ -3767,6 +3790,7 @@ query_tree_mutator_impl(Query *query,
 	MUTATE(query->mergeJoinCondition, query->mergeJoinCondition, Node *);
index fe5a323cfd7..0519106da1f 100644
@@ -313,7 +313,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 									 List *mergeActionLists, List *mergeJoinConditions,
-									 int epqParam);
 
@@ -2833,6 +2833,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);
@@ -7029,7 +7030,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);
 	List	   *fdw_private_list;
@@ -7095,6 +7096,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 		node->exclRelTlist = onconflict->exclRelTlist;
 	}
 	node->updateColnosLists = updateColnosLists;
+	node->forPortionOf = (Node *) forPortionOf;
 	node->withCheckOptionLists = withCheckOptionLists;
 	node->returningLists = returningLists;
 	node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 948afd90948..0a85a89356b 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1988,6 +1988,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 b8b1eae295e..206cfcb4114 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3720,7 +3720,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);
 
@@ -3786,6 +3786,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 28fed9d87f6..4ca77b9d3e9 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,7 +24,10 @@
 
 #include "postgres.h"
 
@@ -47,9 +50,11 @@
 
@@ -58,10 +63,16 @@
 
@@ -476,6 +487,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -509,6 +534,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -547,7 +573,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

 	qry->returningList = transformReturningList(pstate, stmt->returningList,
@@ -1183,7 +1213,7 @@ transformOnConflictClause(ParseState *pstate,
 		 * Now transform the UPDATE subexpressions.
 		 */
 		onConflictSet =
-			transformUpdateTargetList(pstate, onConflictClause->targetList);
+			transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1213,6 +1243,187 @@ 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.
+ *	  - 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;
+	RTEPermissionInfo *target_perminfo = pstate->p_target_nsitem->p_perminfo;
+	char *range_name = forPortionOf->range_name;
+	char *range_type_namespace = NULL;
+	char *range_type_name = NULL;
+	int range_attno = InvalidAttrNumber;
+	Form_pg_attribute attr;
+	Oid	opclass;
+	Oid opfamily;
+	Oid opcintype;
+	Oid funcid = InvalidOid;
+	StrategyNumber strat;
+	Oid	opid;
+	ForPortionOfExpr *result;
+	Var *rangeVar;
+	Node *targetExpr;
+
+	/* 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);
+
+	rangeVar = makeVar(
+			rtindex,
+			range_attno,
+			attr->atttypid,
+			attr->atttypmod,
+			attr->attcollation,
+			0);
+	rangeVar->location = forPortionOf->location;
+	result->rangeVar = rangeVar;
+	result->rangeType = attr->atttypid;
+	if (!get_typname_and_namespace(attr->atttypid, &range_type_name, &range_type_namespace))
+		elog(ERROR, "cache lookup failed for type %u", attr->atttypid);
+
+
+	if (forPortionOf->target)
+		/*
+		 * We were already given an expression for the target,
+		 * so we don't have to build anything.
+		 */
+		targetExpr = forPortionOf->target;
+	else
+	{
+		/* Make sure it's a range column */
+		if (!type_is_range(attr->atttypid))
+			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)));
+
+		/*
+		 * 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.
+		 *
+		 * It also permits UNBOUNDED in either place.
+		 */
+		targetExpr = (Node *) makeFuncCall(
+				list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+				list_make2(forPortionOf->target_start, forPortionOf->target_end),
+				COERCE_EXPLICIT_CALL,
+				forPortionOf->location);
+	}
+	result->targetRange = transformExpr(pstate, targetExpr, EXPR_KIND_UPDATE_PORTION);
+
+	/*
+	 * Build overlapsExpr to use in the whereClause.
+	 * 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);
+	strat = RTOverlapStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass, InvalidOid, &opid, &strat);
+	result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
+			(Node *) copyObject(rangeVar), targetExpr,
+			forPortionOf->location);
+
+	/*
+	 * Look up the withoutPortionOper so we can compute the leftovers.
+	 * Leftovers will be old_range @- target_range
+	 * (one per element of the result).
+	 */
+	funcid = InvalidOid;
+	if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
+		funcid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_WITHOUT_PORTION_PROC);
+
+	if (!OidIsValid(funcid))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("could not identify a without_overlaps support function for type %s", format_type_be(opcintype)),
+				errhint("Define a without_overlaps support function for operator class \"%d\" for access method \"%s\".",
+					 opclass, "gist"));
+
+	result->withoutPortionProc = funcid;
+
+	if (isUpdate)
+	{
+		/*
+		 * Now make sure we update the start/end time of the record.
+		 * For a range col (r) this is `r = r * targetRange`.
+		 */
+		Oid funcnamespace;
+		char *funcname;
+		char *funcnamespacename;
+		Expr *rangeTLEExpr;
+		TargetEntry *tle;
+
+		funcid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_INTERSECT_PROC);
+		if (!OidIsValid(funcid))
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("could not identify an intersect support function for type %s", format_type_be(opcintype)),
+					errhint("Define an intersect support function for operator class \"%d\" for access method \"%s\".",
+						 opclass, "gist"));
+
+		// TODO: do we really need to go through the name here?
+		funcname = get_func_name(funcid);
+		if (!funcname)
+			elog(ERROR, "cache lookup failed for function %u", funcid);
+		funcnamespace = get_func_namespace(funcid, false);
+		funcnamespacename = get_namespace_name(funcnamespace);
+		if (!funcnamespacename)
+			elog(ERROR, "cache lookup failed for namespace %u", funcnamespace);
+
+		rangeTLEExpr = (Expr *) makeFuncCall(
+				list_make2(makeString(funcnamespacename), makeString(funcname)),
+				list_make2(copyObject(rangeVar), targetExpr),
+				COERCE_EXPLICIT_CALL,
+				forPortionOf->location);
+		rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
+
+		/* Make a TLE to set the range column */
+		result->rangeTargetList = NIL;
+		tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
+		result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+		/* Mark the range column as requiring update permissions */
+		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+													  range_attno - FirstLowInvalidHeapAttributeNumber);
+	}
+	else
+		result->rangeTargetList = NIL;
+
+	result->range_name = range_name;
+
+	return result;
+}
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2420,6 +2631,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2437,6 +2649,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2453,7 +2669,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

 	qry->returningList = transformReturningList(pstate, stmt->returningList,
@@ -2463,7 +2680,7 @@ 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);
 
@@ -2482,7 +2699,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2535,6 +2752,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 4addf4d533e..3bcd0c54216 100644
@@ -257,6 +257,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -554,6 +555,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -765,7 +767,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
 
@@ -887,6 +889,17 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * Like the UNBOUNDED PRECEDING/FOLLOWING case, NESTED is assigned a lower
  * precedence than PATH to fix ambiguity in the json_table production.
  */
+/*
+ * We need to handle this shift/reduce conflict:
+ * FOR PORTION OF valid_at FROM INTERVAL YEAR TO MONTH TO foo.
+ * This is basically the classic "dangling else" problem, and we want a
+ * similar resolution: treat the TO as part of the INTERVAL, not as part of
+ * the FROM ... TO .... Users can add parentheses if that's a problem.
+ * TO just needs to be higher precedence than YEAR_P etc.
+ * TODO: I need to figure out a %prec solution before this gets committed!
+ */
+%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P
+%nonassoc TO
 %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
@@ -12353,14 +12366,16 @@ returning_clause:
  *****************************************************************************/

 					n->relation = $4;
-					n->usingClause = $5;
-					n->whereClause = $6;
-					n->returningList = $7;
+					n->forPortionOf = $5;
+					n->usingClause = $6;
+					n->whereClause = $7;
+					n->returningList = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
@@ -12423,6 +12438,7 @@ opt_nowait_or_skip:
  *****************************************************************************/
 
 UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
+			for_portion_of_clause
 			SET set_clause_list
 			from_clause
 			where_or_current_clause
@@ -12431,10 +12447,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					UpdateStmt *n = makeNode(UpdateStmt);
 
 					n->relation = $3;
-					n->targetList = $5;
-					n->fromClause = $6;
-					n->whereClause = $7;
-					n->returningList = $8;
+					n->forPortionOf = $4;
+					n->targetList = $6;
+					n->fromClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
@@ -13903,6 +13920,27 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+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;
+				}
+			| 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;
+				}
+			| /*EMPTY*/					{ $$ = NULL; }
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -17797,6 +17835,7 @@ unreserved_keyword:
 			| PLAN
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -18425,6 +18464,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 bee7d8346a3..00edaf83bab 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -576,6 +576,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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;
 
 			/*
@@ -967,6 +974,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
index 8577f278065..b35781bb247 100644
@@ -586,6 +586,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 			/*
@@ -1859,6 +1862,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3150,6 +3156,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "UPDATE";
 		case EXPR_KIND_MERGE_WHEN:
 			return "MERGE WHEN";
+		case EXPR_KIND_UPDATE_PORTION:
+			return "FOR PORTION OF";
 		case EXPR_KIND_GROUP_BY:
 			return "GROUP BY";
 		case EXPR_KIND_ORDER_BY:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 9b23344a3b1..99921d713be 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2658,6 +2658,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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 87df79027d7..56f676a1f1f 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -385,7 +385,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 					pstate->p_is_insert = false;
 					action->targetList =
 						transformUpdateTargetList(pstate,
-												  mergeWhenClause->targetList);
+												  mergeWhenClause->targetList, NULL);
 				}
 				break;
 			case CMD_DELETE:
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index e1d805d113e..b76253b4198 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3667,6 +3667,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 									  &parsetree->hasSubLinks);
 	}
 
 	 * 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,
@@ -4006,6 +4030,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
index a2cc837ebf9..d5b8666c216 100644
@@ -228,6 +228,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -236,6 +237,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,

 /*
@@ -450,6 +456,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -614,6 +621,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,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -713,6 +721,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
 
@@ -803,9 +813,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -912,6 +929,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1033,6 +1051,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1265,6 +1284,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,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2412,6 +2432,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2467,6 +2488,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3143,3 +3170,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index 48a280d089b..b67c0c03b97 100644
@@ -1629,7 +1629,7 @@ get_func_name(Oid funcid)
  *		Returns the pg_namespace OID associated with a given function.
  */
 Oid
-get_func_namespace(Oid funcid)
+get_func_namespace(Oid funcid, bool missing_ok)
 {
 	HeapTuple	tp;
 
@@ -1643,6 +1643,8 @@ get_func_namespace(Oid funcid)
 		ReleaseSysCache(tp);
 		return result;
 	}
+	else if (!missing_ok)
+		elog(ERROR, "cache lookup failed for function %u", funcid);
 	else
 		return InvalidOid;
 }
@@ -2188,6 +2190,59 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
+ * get_typname
+ *
index 8a47d3c9ec8..b801ef1e3c0 100644
@@ -79,7 +79,7 @@ typedef uint16 StrategyNumber;

 #endif							/* STRATNUM_H */
 
 /*
index 15a42d47dfe..06df800f331 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -429,6 +431,27 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -551,6 +574,9 @@ typedef struct ResultRelInfo
 	/* for MERGE, expr state for checking the join condition */
 	ExprState  *ri_MergeJoinCondition;

diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 1cb7b02c5c2..cc7bfb0f294 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -142,6 +142,9 @@ typedef struct Query
 	 */
 	int			resultRelation pg_node_attr(query_jumble_ignore);
 
@@ -1585,6 +1588,21 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -2057,12 +2075,13 @@ typedef struct InsertStmt
 
@@ -2071,13 +2090,14 @@ typedef struct DeleteStmt
 
index 14ccfc1ac1c..26d06fcad2d 100644
@@ -2385,6 +2385,7 @@ typedef struct ModifyTablePath
index 1aeeaec95e1..d972298cde7 100644
index ea47652adb8..01a101d3776 100644
@@ -2332,4 +2332,30 @@ typedef struct OnConflictExpr
 
+/*----------
index 112e7c23d4e..7a4dc1bc8cc 100644
@@ -285,7 +285,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												List *mergeActionLists, List *mergeJoinConditions,
-												int epqParam);
index 28b66fccb43..6c70dae1452 100644
 extern List *transformReturningList(ParseState *pstate, List *returningList,
 									ParseExprKind exprKind);
index f9a4afd4723..339f094a80d 100644
@@ -344,6 +344,7 @@ PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
index 5b781d87a9d..927f91b3191 100644
index 20446f6f836..e7ad513146a 100644
@@ -121,7 +121,7 @@ extern Oid	get_negator(Oid opno);
-extern Oid	get_func_namespace(Oid funcid);
+extern Oid	get_func_namespace(Oid funcid, bool missing_ok);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
@@ -145,6 +145,8 @@ extern Oid	get_rel_relam(Oid relid);
index 00000000000..5c0a5cf4b1b
@@ -0,0 +1,685 @@
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+  ('[1,2)', NULL, '1 null'),
+  ('[1,2)', '(,)', '1 unbounded'),
+  ('[1,2)', 'empty', '1 empty'),
+('[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')
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[4,5)';
+WHERE id = '[4,5)';
+WHERE id = '[4,5)';
+WHERE id = '[2,3)';
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[1,2)';
+WHERE id = '[6,7)';
+WHERE id = '[3,4)'
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+-- Test with multiranges
+  valid_at datemultirange NOT NULL,
+('[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');
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01')))
+WHERE id = '[1,2)';
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))
+WHERE id = '[2,3)';
+-------+---------------------------------------------------------------------------+-------
+ [1,2) | {[01-02-2018,01-10-2018),[02-10-2018,03-03-2018)}                         | one
+ [1,2) | {[01-10-2018,02-03-2018),[02-04-2018,02-10-2018)}                         | one^1
+ [1,2) | {[03-03-2018,03-05-2018)}                                                 | one
+ [1,2) | {[03-05-2018,04-04-2018)}                                                 | one^1
+ [2,3) | {[01-01-2018,01-15-2018),[02-15-2018,03-01-2018),[03-15-2018,05-01-2018)} | two
+ [3,4) | {[01-01-2018,)}                                                           | three
+('[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');
+WHERE id = '[1,2)';
+WHERE id = '[2,3)';
+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 ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)');
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)');
+  ('[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');
+  WHERE id = '[1,2)';
+  WHERE id = '[3,4)';
+  WHERE id = '[5,6)';
+      id = '[4,5)'
+  WHERE id = '[1,2)';
+      id = '[2,3)'
+  WHERE id = '[3,4)';
+      id = '[3,4)'
+  WHERE id = '[5,6)';
index 9d047b21b88..9b6722c4d58 100644
@@ -1944,6 +1944,7 @@ WHERE a1.amopfamily = 0 OR a1.amoplefttype = 0 OR a1.amoprighttype = 0
index fab0cc800fc..255c581ed7d 100644
@@ -1038,6 +1038,24 @@ ERROR:  null value in column "b" of relation "errtst_part_2" violates not-null c
index 420769a40c9..f973e1554c2 100644
@@ -3666,6 +3666,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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [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;
+ 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
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [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 290e77198ae..902594ef4b8 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -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
@@ -941,6 +971,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
  [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 * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2000-01-01,2000-01-15) | one
+ [1,2) | [2000-01-15,2000-02-01) | one2
+ [1,2) | [2000-02-01,2000-02-15) | one2
+ [1,2) | [2000-02-15,2000-02-20) | one
+ [1,2) | [2000-02-25,2000-03-01) | one
+ [2,3) | [2002-01-01,2003-01-01) | three
+ [3,4) | [2000-01-01,2000-01-15) | three
+ [3,4) | [2000-02-15,2002-01-01) | three
+ [3,4) | [2003-01-01,2010-01-01) | three
+ [4,5) | [2000-02-20,2000-02-25) | one
+(10 rows)
+
 DROP TABLE temporal_partitioned;
 -- temporal UNIQUE:
 CREATE TABLE temporal_partitioned (
@@ -976,6 +1036,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
  [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 * FROM temporal_partitioned ORDER BY id, valid_at;
+  id   |        valid_at         | name  
+-------+-------------------------+-------
+ [1,2) | [2000-01-01,2000-01-15) | one
+ [1,2) | [2000-01-15,2000-02-01) | one2
+ [1,2) | [2000-02-01,2000-02-15) | one2
+ [1,2) | [2000-02-15,2000-02-20) | one
+ [1,2) | [2000-02-25,2000-03-01) | one
+ [2,3) | [2002-01-01,2003-01-01) | three
+ [3,4) | [2000-01-01,2000-01-15) | three
+ [3,4) | [2000-02-15,2002-01-01) | three
+ [3,4) | [2003-01-01,2010-01-01) | three
+ [4,5) | [2000-02-20,2000-02-25) | one
+(10 rows)
+
 DROP TABLE temporal_partitioned;
 -- ALTER TABLE REPLICA IDENTITY
 -- (should fail)
@@ -1520,6 +1610,18 @@ 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".
 -- 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')
@@ -1561,6 +1663,18 @@ 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".
 -- 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')
@@ -1597,9 +1711,22 @@ 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)';
+-- 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".
 -- 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');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
+DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
 -- test FK referenced deletes RESTRICT
 --
@@ -1628,9 +1755,22 @@ BEGIN;
 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".
 ROLLBACK;
+-- 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)';
+-- 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".
 -- 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');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
+DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
 -- test ON UPDATE/DELETE options
 --
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ddc155c195f..5e9693a1d24 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
 
 # ----------
index 00000000000..e802b5c67ff
@@ -0,0 +1,531 @@
+('[1,2)', '[2018-01-02,2020-01-01)', 'one');
+('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one');
+  ('[1,2)', NULL, '1 null'),
+  ('[1,2)', '(,)', '1 unbounded'),
+  ('[1,2)', 'empty', '1 empty'),
+('[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')
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[4,5)';
+WHERE id = '[4,5)';
+WHERE id = '[4,5)';
+WHERE id = '[2,3)';
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+WHERE id = '[1,2)';
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[3,4)';
+WHERE id = '[5,6)';
+WHERE id = '[3,4)';
+WHERE id = '[1,2)';
+WHERE id = '[6,7)';
+WHERE id = '[3,4)'
+WHERE id = '[5,6)';
+WHERE id = '[5,6)';
+INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one');
+-- Test with multiranges
+  valid_at datemultirange NOT NULL,
+('[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');
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01')))
+WHERE id = '[1,2)';
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))
+WHERE id = '[2,3)';
+('[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');
+WHERE id = '[1,2)';
+WHERE id = '[2,3)';
+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 ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)');
+ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)');
+  ('[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');
+  WHERE id = '[1,2)';
+  WHERE id = '[3,4)';
+  WHERE id = '[5,6)';
+      id = '[4,5)'
+  WHERE id = '[1,2)';
+      id = '[2,3)'
+  WHERE id = '[3,4)';
+      id = '[3,4)'
+  WHERE id = '[5,6)';
 
 -- amopmethod must match owning opfamily's opfmethod
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index ae338e8cc8e..7ad687316a4 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -723,6 +723,24 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
 SET SESSION AUTHORIZATION regress_priv_user1;
 DROP TABLE errtst;
 
index 93b693ae837..9bf237c71d4 100644
@@ -1865,6 +1865,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;
+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;
+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;
+
 -- 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 359613c0119..de7186ab455 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -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;
 
 --
@@ -670,6 +684,22 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES
+  WHERE id = '[1,2)';
+  SET id = '[4,5)'
+  SET id = '[2,3)'
+  WHERE id = '[3,4)';
 
 -- temporal UNIQUE:
@@ -688,6 +718,22 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES
+  WHERE id = '[1,2)';
+  SET id = '[4,5)'
+  SET id = '[2,3)'
+  WHERE id = '[3,4)';
 
 -- ALTER TABLE REPLICA IDENTITY
@@ -1138,6 +1184,16 @@ 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)';
 -- 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')
@@ -1177,6 +1233,16 @@ ROLLBACK;
 -- 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)';
 -- 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')
@@ -1212,9 +1278,20 @@ 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)';
+-- 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)';
 -- 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');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
+DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 
 --
 -- test FK referenced deletes RESTRICT
@@ -1243,9 +1320,20 @@ BEGIN;
     DEFERRABLE INITIALLY DEFERRED;
   DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
 ROLLBACK;
+-- 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)';
+-- 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)';
 -- 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');
+-- clean up:
+DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
+DELETE FROM temporal_rng WHERE id IN ('[5,6)');

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

---
 doc/src/sgml/ref/create_table.sgml            |   14 +-
 src/backend/catalog/pg_constraint.c           |   20 +-
 src/backend/commands/tablecmds.c              |   83 +-
 src/backend/utils/adt/ri_triggers.c           |  584 ++++++-
 src/include/catalog/pg_constraint.h           |    7 +-
 src/include/catalog/pg_proc.dat               |   22 +
 src/test/regress/expected/btree_index.out     |   18 +-
 .../regress/expected/without_overlaps.out     | 1551 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     |  902 +++++++++-
 9 files changed, 3085 insertions(+), 116 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 30150eebfa0..c6bc37d6e81 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1265,7 +1265,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>
@@ -1280,7 +1282,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>
@@ -1297,7 +1302,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/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 1e2df031a84..1d7badbf34e 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1351,7 +1351,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.
@@ -1360,11 +1360,14 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
  * aggedcontainedbyoperoid is also a ContainedBy operator,
  * but one whose rhs is a multirange.
  * That way foreign keys can compare fkattr <@ range_agg(pkattr).
+ * 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)
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *containedbyoperoid,
+						  Oid *aggedcontainedbyoperoid,
+						  Oid *intersectprocoid)
 {
 	Oid			opfamily = InvalidOid;
 	Oid			opcintype = InvalidOid;
@@ -1405,6 +1408,15 @@ FindFKPeriodOpers(Oid opclass,
 									 ANYMULTIRANGEOID,
 									 aggedcontainedbyoperoid,
 									 &strat);
+
+	/*
+	 * If the command uses FOR PORTION OF,
+	 * we will also need an intersect support proc.
+	 * If this is missing we don't need to complain here,
+	 * because FOR PORTION OF will not be allowed.
+	 */
+	if (opfamily != InvalidOid && opcintype != InvalidOid)
+		*intersectprocoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_INTERSECT_PROC);
 }
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8da83125262..d7bb1df259b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -515,7 +515,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
 											bool old_check_ok,
 											Oid parentDelTrigger, Oid parentUpdTrigger,
 											bool with_period);
-static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, const int16 fkperiodattnum,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
 static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -9595,6 +9595,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,
@@ -9690,15 +9691,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
 	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];
+	}

@@ -9780,28 +9785,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
-	/*
-	 * Some actions are currently unsupported for foreign keys using PERIOD.
-	 */
-	if (fkconstraint->fk_with_period)
-	{
-		if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL ||
-			fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("unsupported %s action for foreign key constraint using PERIOD",
-						   "ON UPDATE"));
-
-		if (fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL ||
-			fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT)
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("unsupported %s action for foreign key constraint using PERIOD",
-						   "ON DELETE"));
-	}
-
 	/*
 	 * Look up the equality operators to use in the constraint.
 	 *
@@ -10059,8 +10042,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
+		Oid			intersectprocoid;
 
-		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1],
+								  &periodoperoid,
+								  &aggedperiodoperoid,
+								  &intersectprocoid);
 	}
 
 	/*
@@ -10114,6 +10101,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10124,6 +10112,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *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;
@@ -12452,17 +12447,26 @@ createForeignKeyActionTriggers(Relation rel, 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",
@@ -12513,17 +12517,26 @@ createForeignKeyActionTriggers(Relation rel, 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 d5b8666c216..9613bfaa71b 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -80,6 +80,12 @@
 #define RI_PLAN_SETNULL_ONUPDATE		7
 #define RI_PLAN_SETDEFAULT_ONDELETE		8
 #define RI_PLAN_SETDEFAULT_ONUPDATE		9
+#define RI_PLAN_PERIOD_CASCADE_ONDELETE		10
+#define RI_PLAN_PERIOD_CASCADE_ONUPDATE		11
+#define RI_PLAN_PERIOD_SETNULL_ONUPDATE		12
+#define RI_PLAN_PERIOD_SETNULL_ONDELETE		13
+#define RI_PLAN_PERIOD_SETDEFAULT_ONUPDATE	14
+#define RI_PLAN_PERIOD_SETDEFAULT_ONDELETE	15
 
 #define MAX_QUOTED_NAME_LEN  (NAMEDATALEN*2+3)
 #define MAX_QUOTED_REL_NAME_LEN  (MAX_QUOTED_NAME_LEN*2)
@@ -128,7 +134,8 @@ typedef struct RI_ConstraintInfo
 	Oid			pp_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = PK) */
 	Oid			ff_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (FK = FK) */
 	Oid			period_contained_by_oper;	/* anyrange <@ anyrange */
-	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
+	Oid			agged_period_contained_by_oper;	/* fkattr <@ range_agg(pkattr) */
+	Oid			period_intersect_proc;	/* anyrange * anyrange (or multirange) */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -192,6 +199,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,
@@ -239,7 +247,7 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 							   int queryno, bool partgone) pg_attribute_noreturn();
 static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal,
 								 const RI_ConstraintInfo *riinfo);
-static Datum restrict_cascading_range(const ForPortionOfState *tg_temporal,
+static Datum restrict_cascading_range(Relation rel, const ForPortionOfState *tg_temporal,
 									  const RI_ConstraintInfo *riinfo,
 									  TupleTableSlot *oldslot);
 
@@ -816,7 +824,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	if (trigdata->tg_temporal)
 	{
 		targetRangeParam = riinfo->nkeys - 1;
-		targetRange = restrict_cascading_range(trigdata->tg_temporal, riinfo, oldslot);
+		targetRange = restrict_cascading_range(pk_rel, trigdata->tg_temporal, riinfo, oldslot);
 	}
 
 	ri_PerformCheck(riinfo, &qkey, qplan,
@@ -1315,6 +1323,535 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
+ * RI_FKey_period_cascade_del -
+ *
+RI_FKey_period_cascade_del(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_cascade_del", RI_TRIGTYPE_DELETE);
+	targetRange = restrict_cascading_range(pk_rel, trigdata->tg_temporal, riinfo, oldslot);
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_PERIOD_CASCADE_ONDELETE);
+		 *  FOR PORTION OF $fkatt (${n+1})
+		appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s ($%d)",
+						 fk_only, fkrelname, attname, riinfo->nkeys + 1);
+ * RI_FKey_period_cascade_upd -
+RI_FKey_period_cascade_upd(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_cascade_upd", RI_TRIGTYPE_UPDATE);
+	targetRange = restrict_cascading_range(pk_rel, trigdata->tg_temporal, riinfo, oldslot);
+	ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_PERIOD_CASCADE_ONUPDATE);
+		 *		  FOR PORTION OF $fkatt (${2n+1})
+		appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s ($%d) SET",
+						 fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1);
+ * RI_FKey_period_setnull_del -
+RI_FKey_period_setnull_del(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setnull_del", RI_TRIGTYPE_DELETE);
+ * RI_FKey_period_setnull_upd -
+RI_FKey_period_setnull_upd(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setnull_upd", RI_TRIGTYPE_UPDATE);
+ * RI_FKey_period_setdefault_del -
+RI_FKey_period_setdefault_del(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setdefault_del", RI_TRIGTYPE_DELETE);
+ * RI_FKey_period_setdefault_upd -
+RI_FKey_period_setdefault_upd(PG_FUNCTION_ARGS)
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_setdefault_upd", RI_TRIGTYPE_UPDATE);
+	targetRange = restrict_cascading_range(pk_rel, trigdata->tg_temporal, riinfo, oldslot);
+				? RI_PLAN_PERIOD_SETNULL_ONUPDATE
+				: RI_PLAN_PERIOD_SETDEFAULT_ONUPDATE;
+				? RI_PLAN_PERIOD_SETNULL_ONDELETE
+				: RI_PLAN_PERIOD_SETDEFAULT_ONDELETE;
+		 *			FOR PORTION OF $fkatt (${n+1})
+		appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s ($%d) SET",
+						 fk_only, fkrelname, attname, riinfo->nkeys + 1);
@@ -2287,9 +2824,10 @@ 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);
+		FindFKPeriodOpersAndProcs(opclass,
+								  &riinfo->period_contained_by_oper,
+								  &riinfo->agged_period_contained_by_oper,
+								  &riinfo->period_intersect_proc);
 	}
 
 	ReleaseSysCache(tup);
@@ -2444,8 +2982,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
 
 	/*
 	 * Use the query type code to determine whether the query is run against
@@ -2480,8 +3018,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 		ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk,
 						 vals, nulls);
 		if (oldslot)
+		{
 			ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk,
 							 vals + riinfo->nkeys, nulls + riinfo->nkeys);
+		}
 	}
 	else
 	{
@@ -3161,6 +3701,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:
@@ -3189,30 +3735,30 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn
 /*
  * restrict_cascading_range -
  *
- * Returns a Datum of RangeTypeP holding the appropriate timespan
- * to target child records when we CASCADE/SET NULL/SET DEFAULT.
+ * Returns a Datum holding the appropriate timespan
+ * to target referencing records when we CASCADE/SET NULL/SET DEFAULT.
  *
- * In a normal UPDATE/DELETE this should be the parent's own valid time,
+ * 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 parent's span further.
+ * trim down the span further.
  */
 static Datum
-restrict_cascading_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+restrict_cascading_range(Relation rel, const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
 {
 	Datum	pkRecordRange;
 	bool	isnull;
+	int		attno = riinfo->pk_attnums[riinfo->nkeys - 1];
 
-	pkRecordRange = slot_getattr(oldslot, riinfo->pk_attnums[riinfo->nkeys - 1], &isnull);
+	pkRecordRange = slot_getattr(oldslot, attno, &isnull);
 	if (isnull)
 		elog(ERROR, "application time should not be null");
 
 	if (fpo_targets_pk_range(tg_temporal, riinfo))
 	{
-		RangeType *r1 = DatumGetRangeTypeP(pkRecordRange);
-		RangeType *r2 = DatumGetRangeTypeP(tg_temporal->fp_targetRange);
-		Oid rngtypid = RangeTypeGetOid(r1);
-		TypeCacheEntry *typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO);
-		return RangeTypePGetDatum(range_intersect_internal(typcache, r1, r2));
+		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_constraint.h b/src/include/catalog/pg_constraint.h
index 115217a6162..a56f665e149 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -279,9 +279,10 @@ 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);
+extern void FindFKPeriodOpersAndProcs(Oid opclass,
+									  Oid *containedbyoperoid,
+									  Oid *aggedcontainedbyoperoid,
+									  Oid *intersectprocoid);
 
 extern bool check_functional_grouping(Oid relid,
 									  Index varno, Index varlevelsup,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34674686ad3..57347ec260e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3960,6 +3960,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 510646cbce7..5796333f4f0 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -258,14 +258,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;
@@ -304,14 +307,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 902594ef4b8..bbd222db361 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
@@ -1622,6 +1622,21 @@ 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')
@@ -1675,6 +1690,21 @@ 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')
@@ -1721,12 +1751,22 @@ 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
 -- test FK referenced deletes RESTRICT
 --
@@ -1765,39 +1805,206 @@ 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
--- test ON UPDATE/DELETE options
+-- 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 ('[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
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
 	ADD CONSTRAINT temporal_fk_rng2rng_fk
 		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
-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
 	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 ('[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]',
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
@@ -1805,7 +2012,432 @@ ALTER TABLE temporal_fk_rng2rng
 		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
 --
@@ -2125,6 +2757,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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 --
 -- test FK referenced updates RESTRICT
 --
@@ -2162,6 +2810,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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 --
 -- test FK referenced deletes NO ACTION
 --
@@ -2194,6 +2858,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'));
 --
 -- test FK referenced deletes RESTRICT
 --
@@ -2222,6 +2899,638 @@ BEGIN;
 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".
 ROLLBACK;
+-- 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'));
+--
+-- 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
 --
@@ -2231,8 +3540,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
 	CONSTRAINT temporal_paritioned_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'),
@@ -2245,8 +3554,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
 --
@@ -2288,7 +3597,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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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
@@ -2300,7 +3609,7 @@ 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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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 RESTRICT
@@ -2322,7 +3631,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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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 RESTRICT
@@ -2334,48 +3643,234 @@ 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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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]',
+  ALTER COLUMN parent_id SET DEFAULT '[0,1)',
 	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 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)';
+SELECT * FROM temporal_partitioned_fk_rng2rng 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) | [12,13)
+ [8,9) | [2020-01-01,2021-01-01) | [12,13)
+(3 rows)
+
+UPDATE temporal_partitioned_rng SET id = '[13,14)' WHERE id = '[12,13)';
+SELECT * FROM temporal_partitioned_fk_rng2rng 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) | [0,1)
+ [8,9) | [2020-01-01,2021-01-01) | [0,1)
+(3 rows)
+
+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)';
+   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_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)';
+   id   |        valid_at         | parent_id 
+--------+-------------------------+-----------
+ [9,10) | [2019-01-01,2020-01-01) | [0,1)
+ [9,10) | [2018-01-01,2019-01-01) | [14,15)
+ [9,10) | [2020-01-01,2021-01-01) | [14,15)
+(3 rows)
+
+DELETE FROM temporal_partitioned_rng WHERE id = '[14,15)';
+SELECT * FROM temporal_partitioned_fk_rng2rng 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) | [0,1)
+ [9,10) | [2020-01-01,2021-01-01) | [0,1)
+(3 rows)
+
+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)';
+   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_rng2rng;
 DROP TABLE temporal_partitioned_rng;
 RESET datestyle;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index de7186ab455..c8241c27929 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;
 
@@ -1194,6 +1194,8 @@ 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')
@@ -1243,6 +1245,8 @@ 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')
@@ -1286,12 +1290,11 @@ WHERE id = '[5,6)';
 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 
 --
 -- test FK referenced deletes RESTRICT
@@ -1328,41 +1331,131 @@ WHERE id = '[5,6)';
 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 
 --
--- test ON UPDATE/DELETE options
+-- 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 ('[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
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
 	ADD CONSTRAINT temporal_fk_rng2rng_fk
 		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
-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
 	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 ('[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]',
 	DROP CONSTRAINT temporal_fk_rng2rng_fk,
@@ -1370,6 +1463,252 @@ ALTER TABLE temporal_fk_rng2rng
 		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
@@ -1662,6 +2001,20 @@ 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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 
 --
 -- test FK referenced updates RESTRICT
@@ -1698,6 +2051,20 @@ ROLLBACK;
 -- 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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 
 --
 -- test FK referenced deletes NO ACTION
@@ -1729,6 +2096,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'));
 
 --
 -- test FK referenced deletes RESTRICT
@@ -1758,6 +2136,417 @@ BEGIN;
 
   DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 ROLLBACK;
+-- 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'));
+
+--
+-- 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
@@ -1769,8 +2558,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
 	CONSTRAINT temporal_paritioned_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'),
@@ -1784,8 +2573,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
@@ -1878,48 +2667,125 @@ 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]',
+  ALTER COLUMN parent_id SET DEFAULT '[0,1)',
 	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 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;
 
-- 
2.42.0

From 50bdc288b844a9c6855c2358f36909a102bfd073 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v37 8/8] Add PERIODs

- Added parsing for SQL:2011 syntax to define an application-time PERIOD on a
  table (in both CREATE TABLE and ALTER TABLE). Make sure we create the PERIOD
  after columns are known (since PERIODs can refer to them) but before
  constraints are handled (since PERIODs can appear in them).
- Added ALTER TABLE DROP support for PERIODs.
- Created postgres.pg_period table.
- Created information_schema.periods view.
- Added pg_dump support.
- Added tests and documentation.
- Automatically define a constraint for each PERIOD requiring the start column
  to be less than the end column.
- When creating a PERIOD, choose an appropriate range type we can use to
  implement PERIOD-related operations. You can choose one explicitly if there
  is ambiguity (due to multiple range types created over the same base type).
---
 doc/src/sgml/catalogs.sgml                    |  112 ++
 doc/src/sgml/ddl.sgml                         |   58 +
 doc/src/sgml/information_schema.sgml          |   63 +
 doc/src/sgml/ref/alter_table.sgml             |   25 +
 doc/src/sgml/ref/comment.sgml                 |    2 +
 doc/src/sgml/ref/create_table.sgml            |   61 +-
 doc/src/sgml/ref/delete.sgml                  |    3 +-
 doc/src/sgml/ref/update.sgml                  |    3 +-
 src/backend/catalog/Makefile                  |    1 +
 src/backend/catalog/aclchk.c                  |    2 +
 src/backend/catalog/dependency.c              |   14 +
 src/backend/catalog/heap.c                    |   75 +
 src/backend/catalog/information_schema.sql    |   23 +-
 src/backend/catalog/meson.build               |    1 +
 src/backend/catalog/objectaddress.c           |   72 +
 src/backend/catalog/pg_period.c               |  132 ++
 src/backend/catalog/sql_features.txt          |    2 +-
 src/backend/commands/comment.c                |   10 +
 src/backend/commands/dropcmds.c               |    1 +
 src/backend/commands/event_trigger.c          |    2 +
 src/backend/commands/seclabel.c               |    1 +
 src/backend/commands/tablecmds.c              |  866 +++++++-
 src/backend/commands/view.c                   |    4 +-
 src/backend/executor/nodeModifyTable.c        |   42 +-
 src/backend/nodes/nodeFuncs.c                 |    3 +
 src/backend/parser/analyze.c                  |   99 +-
 src/backend/parser/gram.y                     |   45 +-
 src/backend/parser/parse_relation.c           |   10 +
 src/backend/parser/parse_utilcmd.c            |  180 +-
 src/backend/utils/adt/Makefile                |    1 +
 src/backend/utils/adt/period.c                |   56 +
 src/backend/utils/cache/lsyscache.c           |   87 +
 src/bin/pg_dump/pg_backup_archiver.c          |    1 +
 src/bin/pg_dump/pg_dump.c                     |  177 +-
 src/bin/pg_dump/pg_dump.h                     |   15 +
 src/bin/pg_dump/pg_dump_sort.c                |    7 +
 src/bin/psql/describe.c                       |   38 +-
 src/include/catalog/Makefile                  |    1 +
 src/include/catalog/heap.h                    |    4 +
 src/include/catalog/meson.build               |    1 +
 src/include/catalog/pg_index.h                |    1 -
 src/include/catalog/pg_period.h               |   58 +
 src/include/catalog/pg_range.h                |    2 +
 src/include/commands/tablecmds.h              |    4 +-
 src/include/nodes/parsenodes.h                |   40 +-
 src/include/nodes/primnodes.h                 |    2 +
 src/include/parser/kwlist.h                   |    2 +-
 src/include/parser/parse_utilcmd.h            |    1 +
 src/include/utils/lsyscache.h                 |    3 +
 src/include/utils/period.h                    |   21 +
 .../test_ddl_deparse/test_ddl_deparse.c       |    6 +
 src/test/regress/expected/for_portion_of.out  |   38 +
 src/test/regress/expected/generated.out       |    4 +-
 src/test/regress/expected/periods.out         |  267 +++
 .../regress/expected/without_overlaps.out     | 1766 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/for_portion_of.sql       |   32 +
 src/test/regress/sql/periods.sql              |  178 ++
 src/test/regress/sql/without_overlaps.sql     | 1190 +++++++++++
 59 files changed, 5847 insertions(+), 70 deletions(-)
 create mode 100644 src/backend/catalog/pg_period.c
 create mode 100644 src/backend/utils/adt/period.c
 create mode 100644 src/include/catalog/pg_period.h
 create mode 100644 src/include/utils/period.h
 create mode 100644 src/test/regress/expected/periods.out
 create mode 100644 src/test/regress/sql/periods.sql

index 84a2f7e4106..55301c02c1e 100644
 
+     <row>
@@ -5748,6 +5753,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
index 626d35514cc..d6ec6f5df5b 100644
@@ -1356,6 +1356,64 @@ CREATE TABLE circles (
   </sect2>
  </sect1>

index 9442b0718c0..b1f8a1413c0 100644

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 6a2822adad7..332b04f186f 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
     VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
     DROP CONSTRAINT [ IF EXISTS ]  <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+    ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+    DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
     DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -591,6 +593,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index c6bc37d6e81..f859471f756 100644

 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
@@ -80,7 +88,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
   UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
 class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -139,6 +147,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    name as any existing data type in the same schema.
   </para>
 
+  <para>
@@ -799,6 +815,37 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-period">
+    <term><literal>PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">column_name</replaceable>, <replaceable class="parameter">column_name</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]</literal></term>
+    <listitem>
+     <para>
+      A period definition gives semantic meaning to two existing columns of
+      the table.  It defines a "start column" and an "end column" where the
+      start value is strictly less than the end value.  A
+      <literal>CHECK</literal> constraint is automatically created to enforce
+      this. You can specify the name of that constraint with the
+      <literal>check_constraint_name</literal> <replaceable class="parameter">period_option</replaceable>.
+     </para>
+
+     <para>
+      Both columns must have exactly the same type and must have a range type
+      defined from their base type.  If there are several range types for that
+      base type, you must specify which one you want by using the
+      <literal>rangetype</literal> <replaceable class="parameter">period_option</replaceable>.
+      Any base type is allowed, as long as it has a range type, although it is
+      expected that most periods will use temporal types like <literal>timestamptz</literal>
+      or <literal>date</literal>.
+     </para>
+
+     <para>
+      A hidden <literal>GENERATED</literal> column is used to implement the period,
+      with the same name as the period.  If such a column already exists, you
+      can use the <literal>colexists</literal> <replaceable class="parameter">period_option</replaceable>
+      with value <literal>true</literal> to avoid creating it.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="sql-createtable-parms-constraint">
     <term><literal>CONSTRAINT <replaceable class="parameter">constraint_name</replaceable></literal></term>
     <listitem>
@@ -1147,8 +1194,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    <varlistentry id="sql-createtable-parms-references">
     <term><literal>REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> ) ] [ MATCH <replaceable class="parameter">matchtype</replaceable> ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal> (column constraint)</term>
 
-   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] )
-    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
+   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] )
+    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH <replaceable class="parameter">matchtype</replaceable> ]
     [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
     [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1168,7 +1215,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
-      If the last column is marked with <literal>PERIOD</literal>, it is
+      If the last key part is marked with <literal>PERIOD</literal>, it is
       treated in a special way.  While the non-<literal>PERIOD</literal>
       columns are compared for equality (and there must be at least one of
       them), the <literal>PERIOD</literal> column is not.  Instead, the
@@ -1176,8 +1223,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       records (based on the non-<literal>PERIOD</literal> parts of the key)
       whose combined <literal>PERIOD</literal> values completely cover the
       referencing record's.  In other words, the reference must have a
-      referent for its entire duration.  This column must be a range or
-      multirange type.  In addition, the referenced table must have a primary
+      referent for its entire duration.  This part must be either a 
+      <link linkend="sql-createtable-parms-period"><literal>PERIOD</literal></link>
+      or a column with a range or multirange type.
+      In addition, the referenced table must have a primary
       key or unique constraint declared with <literal>WITHOUT
       OVERLAPS</literal>. Finally, if the foreign key has a PERIOD
       <replaceable class="parameter">column_name</replaceable> specification
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 0355b6085f1..c5777649b9e 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -56,7 +56,8 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
   </para>
 
   <para>
index ed8c5748a26..46b0ff981a6 100644
 
   <para>
index 1589a75fd53..3b7af965032 100644
index a44ccee3b68..d57c4fad52a 100644
@@ -2822,6 +2822,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
@@ -2963,6 +2964,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
index 0489cbabcb8..c7f26bc7189 100644
@@ -622,6 +623,15 @@ findDependentObjects(const ObjectAddress *object,
 						return;
 					}
 
+					/*
@@ -1400,6 +1410,10 @@ doDeletion(const ObjectAddress *object, int flags)
 
+		case PeriodRelationId:
+			RemovePeriodById(object->objectId);
+			break;
+
 		case LargeObjectRelationId:
 			LargeObjectDrop(object->objectId);
 			break;
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 78e59384d1c..d3a667039dd 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_partitioned_table.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_tablespace.h"
@@ -2062,6 +2063,80 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 	table_close(tablerel, AccessExclusiveLock);
 }
 
+/*
index c4145131ce4..41f1fb3673f 100644
@@ -1211,7 +1211,28 @@ GRANT SELECT ON parameters TO PUBLIC;
  * PERIODS view
  */

 /*
index 85a7b7e641a..45b628dc558 100644
@@ -45,6 +45,7 @@
@@ -727,6 +728,9 @@ static const struct object_type_map
@@ -970,6 +974,7 @@ get_object_address(ObjectType objtype, Node *object,
@@ -1468,6 +1473,13 @@ get_object_address_relobject(ObjectType objtype, List *object,
@@ -2285,6 +2297,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
@@ -2395,6 +2408,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
@@ -3043,6 +3057,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
@@ -4474,6 +4520,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 										 missing_ok);
 			break;
 
+		case PeriodRelationId:
+			appendStringInfoString(&buffer, "period");
+			break;
+
 		case ConversionRelationId:
 			appendStringInfoString(&buffer, "conversion");
 			break;
@@ -4979,6 +5029,28 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index c002f37202f..9a74b0fa240 100644
@@ -448,7 +448,7 @@ T176	Sequence generator support			NO	supported except for NEXT VALUE FOR
index 85eec7e3947..3d7879f93c1 100644
@@ -504,6 +504,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
index 7a5ed6b9850..d4d36cfd5bb 100644
@@ -2176,6 +2176,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2260,6 +2261,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index d7bb1df259b..4a43717c25e 100644
 #include "catalog/pg_publication_rel.h"
@@ -154,6 +155,11 @@ typedef enum AlterTablePass
 							 bool is_partition, List **supconstr);
 static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
 static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
@@ -444,6 +451,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
+											bool colexists, bool if_not_exists);
 static void ATPrepDropNotNull(Relation rel, bool recurse, bool recursing);
@@ -463,6 +472,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -662,6 +677,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 
 static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
 								 Relation rel, PartitionCmd *cmd,
@@ -888,6 +907,79 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints);
 
+	/*
+	 * Using the column list (including inherited columns),
+	 * find the start/end columns for each period.
+	 * PERIODs should be inherited too (but aren't yet).
+	 */
+	stmt->periods = MergePeriods(relname, stmt->periods, stmt->tableElts, inheritOids);
+
+	/*
+	 * For each PERIOD we need a GENERATED column.
+	 * Usually we must create this, so we add it to tableElts.
+	 * If the user says the column already exists,
+	 * make sure it is sensible.
+	 * These columns are not inherited,
+	 * so we don't worry about conflicts in tableElts.
+	 *
+	 * We allow this colexists option to support pg_upgrade,
+	 * so we have more control over the GENERATED column
+	 * (whose attnum must match the old value).
+	 */
+	foreach(listptr, stmt->periods)
+	{
+		PeriodDef *period = (PeriodDef *) lfirst(listptr);
+
+		if (period->colexists)
+		{
+			ListCell *cell;
+			bool found = false;
+
+			foreach(cell, stmt->tableElts)
+			{
+				ColumnDef  *colDef = lfirst(cell);
+
+				if (strcmp(period->periodname, colDef->colname) == 0)
+				{
+					/*
+					 * Lots to check here:
+					 * It must be GENERATED ALWAYS,
+					 * it must have the right expression,
+					 * it must be the right type,
+					 * it must be NOT NULL,
+					 * it must not be inherited.
+					 */
+					if (colDef->generated == '\0')
+						ereport(ERROR, (errmsg("Period %s uses a non-generated column", period->periodname)));
+
+					if (colDef->generated != ATTRIBUTE_GENERATED_STORED)
+						ereport(ERROR, (errmsg("Period %s uses a generated column that is not STORED", period->periodname)));
+
+					// TODO: check the GENERATED expression also.
+
+					if (!colDef->is_not_null && !IsBinaryUpgrade)
+						ereport(ERROR, (errmsg("Period %s uses a generated column that allows nulls", period->periodname)));
+
+					if (period->rngtypid != typenameTypeId(NULL, colDef->typeName))
+						ereport(ERROR, (errmsg("Period %s uses a generated column with the wrong type", period->periodname)));
+
+					if (!colDef->is_local)
+						ereport(ERROR, (errmsg("Period %s uses a generated column that is inherited", period->periodname)));
+
+					found = true;
+				}
+			}
+
+			if (!found)
+				ereport(ERROR, (errmsg("No column found with name %s", period->periodname)));
+		}
+		else
+		{
+			ColumnDef *col = make_range_column_for_period(period);
+			stmt->tableElts = lappend(stmt->tableElts, col);
+		}
+	}
+
 	/*
 	 * Create a tuple descriptor from the relation schema.  Note that this
 	 * deals with column names, types, and not-null constraints, but not
@@ -1263,6 +1355,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		AddRelationNewConstraints(rel, NIL, stmt->constraints,
 								  true, true, false, queryString);
 
+	/*
 
 	/*
@@ -1360,6 +1467,312 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
+ * If either bound is NULL, the constraint passes
+ * (since that indicates an unbounded range).
+ * rngtypid - the range type to use.
+ * rngattnum - the attnum of a pre-existing range column, or Invalid.
+	/* Get the range type based on the start/end cols or the user's choice */
+	/* If the GENERATED columns should already exist, make sure it is sensible. */
+	if (period->colexists)
+		HeapTuple rngtuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->periodname);
+		if (!HeapTupleIsValid(rngtuple))
+							period->periodname, RelationGetRelationName(rel))));
+		atttuple = (Form_pg_attribute) GETSTRUCT(rngtuple);
+		 * Lots to check here:
+		 * It must be GENERATED ALWAYS,
+		 * it must have the right expression,
+		 * it must be the right type,
+		 * it must be NOT NULL,
+		 * it must not be inherited.
+		if (atttuple->attgenerated == '\0')
+			ereport(ERROR, (errmsg("Period %s uses a non-generated column", period->periodname)));
+		if (atttuple->attgenerated != ATTRIBUTE_GENERATED_STORED)
+			ereport(ERROR, (errmsg("Period %s uses a generated column that is not STORED", period->periodname)));
+		// TODO: check the GENERATED expression also.
+		if (!atttuple->attnotnull && !IsBinaryUpgrade)
+			ereport(ERROR, (errmsg("Period %s uses a generated column that allows nulls", period->periodname)));
+		if (period->rngtypid != atttuple->atttypid)
+			ereport(ERROR, (errmsg("Period %s uses a generated column with the wrong type", period->periodname)));
+		if (!atttuple->attislocal)
+			ereport(ERROR, (errmsg("Period %s uses a generated column that is inherited", period->periodname)));
+		period->rngattnum = atttuple->attnum;
+		heap_freetuple(rngtuple);
@@ -3008,6 +3421,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
+			if (!period->colexists && strcmp(period->periodname, col->colname) == 0)
@@ -4389,12 +4964,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4403,7 +4978,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4496,6 +5071,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4828,6 +5405,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5237,6 +5822,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6406,6 +6999,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6431,6 +7026,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7408,14 +8005,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7459,6 +8071,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
+								bool colexists, bool if_not_exists)
+		if (!colexists)
@@ -7989,6 +8672,157 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
+	(void) check_for_period_name_collision(rel, period->periodname, period->colexists, false);
+	if (!period->colexists)
@@ -9763,8 +10597,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	for (i = 0; i < numfks; i++)
 	{
 		char		attgenerated = TupleDescAttr(RelationGetDescr(rel), fkattnum[i] - 1)->attgenerated;
+		Bitmapset  *periods = get_period_attnos(RelationGetRelid(rel));
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -13548,6 +14383,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 			case ProcedureRelationId:
 
 				/*
@@ -13637,6 +14482,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -15530,7 +16384,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index fdad8338324..3ae225d6798 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -164,7 +164,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			}

 			/* Make the new view columns visible */
 			CommandCounterIncrement();
@@ -196,7 +196,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		atcmds = list_make1(atcmd);

 		/*
 		 * There is very little to do here to update the view's dependencies.
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index a0f29285f45..cf25004bfbd 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1355,8 +1355,46 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 		}
 
 		/* store the new range */
-		leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
-		leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+		if (forPortionOf->startVar)
+		{
+			/* For PERIODs we must split the range into start and end columns */
+
+			RangeType  *r = DatumGetRangeTypeP(leftover);
+			RangeBound	lower;
+			RangeBound	upper;
+			bool		empty;
+
+			range_deserialize(typcache, r, &lower, &upper, &empty);
+
+			if (lower.infinite)
+			{
+				leftoverSlot->tts_values[forPortionOf->startVar->varattno - 1] = 0;
+				leftoverSlot->tts_isnull[forPortionOf->startVar->varattno - 1] = true;
+			}
+			else
+			{
+				leftoverSlot->tts_values[forPortionOf->startVar->varattno - 1] = lower.val;
+				leftoverSlot->tts_isnull[forPortionOf->startVar->varattno - 1] = false;
+			}
+
+			if (upper.infinite)
+			{
+				leftoverSlot->tts_values[forPortionOf->endVar->varattno - 1] = 0;
+				leftoverSlot->tts_isnull[forPortionOf->endVar->varattno - 1] = true;
+			}
+			else
+			{
+				leftoverSlot->tts_values[forPortionOf->endVar->varattno - 1] = upper.val;
+				leftoverSlot->tts_isnull[forPortionOf->endVar->varattno - 1] = false;
+			}
+		}
+		else
+		{
+			/* Just store into the range/whatever column */
+
+			leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+			leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+		}
 		ExecMaterializeSlot(leftoverSlot);
 
 		/*
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 8c56dc67c30..15bbc146880 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1722,6 +1722,9 @@ exprLocation(const Node *expr)
 		case T_Constraint:
 			loc = ((const Constraint *) expr)->location;
 			break;
+		case T_PeriodDef:
+			loc = ((const PeriodDef *) expr)->location;
+			break;
 		case T_FunctionParameter:
 			/* just use typename's location */
 			loc = exprLocation((Node *) ((const FunctionParameter *) expr)->argType);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 4ca77b9d3e9..42190e76e6b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -28,6 +28,7 @@
 #include "access/stratnum.h"
 #include "access/sysattr.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
@@ -1265,7 +1266,11 @@ transformForPortionOfClause(ParseState *pstate,
 	char *range_name = forPortionOf->range_name;
 	char *range_type_namespace = NULL;
 	char *range_type_name = NULL;
-	int range_attno = InvalidAttrNumber;
+	AttrNumber range_attno = InvalidAttrNumber;
+	AttrNumber start_attno = InvalidAttrNumber;
+	AttrNumber end_attno = InvalidAttrNumber;
+	char *startcolname = NULL;
+	char *endcolname = NULL;
 	Form_pg_attribute attr;
 	Oid	opclass;
 	Oid opfamily;
@@ -1309,6 +1314,53 @@ transformForPortionOfClause(ParseState *pstate,
 	if (!get_typname_and_namespace(attr->atttypid, &range_type_name, &range_type_namespace))
 		elog(ERROR, "cache lookup failed for type %u", attr->atttypid);
 
+	/*
+	 * If we are using a PERIOD, we need the start & end columns.
+	 * If the attribute it not a GENERATED column, we needn't query pg_period.
+	 */
+	if (attr->attgenerated == ATTRIBUTE_GENERATED_STORED)
+	{
+		HeapTuple perTuple = SearchSysCache2(PERIODNAME,
+											 ObjectIdGetDatum(RelationGetRelid(targetrel)),
+											 PointerGetDatum(range_name));
+		if (HeapTupleIsValid(perTuple))
+		{
+			Form_pg_period per = (Form_pg_period) GETSTRUCT(perTuple);
+			Form_pg_attribute perattr;
+
+			start_attno = per->perstart;
+			end_attno = per->perend;
+
+			perattr = TupleDescAttr(targetrel->rd_att, start_attno - 1);
+			startcolname = NameStr(perattr->attname);
+
+			result->startVar = makeVar(
+					rtindex,
+					start_attno,
+					perattr->atttypid,
+					perattr->atttypmod,
+					perattr->attcollation,
+					0);
+
+			perattr = TupleDescAttr(targetrel->rd_att, end_attno - 1);
+			endcolname = NameStr(perattr->attname);
+			result->endVar = makeVar(
+					rtindex,
+					end_attno,
+					perattr->atttypid,
+					perattr->atttypmod,
+					perattr->attcollation,
+					0);
+
+			ReleaseSysCache(perTuple);
+		}
+	}
+
+	if (start_attno == InvalidAttrNumber)
+	{
+		result->startVar = NULL;
+		result->endVar = NULL;
+	}
 
 	if (forPortionOf->target)
 		/*
@@ -1376,7 +1428,10 @@ transformForPortionOfClause(ParseState *pstate,
 	{
 		/*
 		 * Now make sure we update the start/end time of the record.
-		 * For a range col (r) this is `r = r * targetRange`.
+		 * For a range/etc col (r) this is `r = r * targetRange`.
+		 * For a PERIOD with cols (s, e) this is `s = lower(tsrange(s, e) * targetRange)`
+		 * and `e = upper(tsrange(s, e) * targetRange` (of course not necessarily with
+		 * tsrange, but with whatever range type is used there).
 		 */
 		Oid funcnamespace;
 		char *funcname;
@@ -1406,12 +1461,44 @@ transformForPortionOfClause(ParseState *pstate,
 				list_make2(copyObject(rangeVar), targetExpr),
 				COERCE_EXPLICIT_CALL,
 				forPortionOf->location);
-		rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
 
-		/* Make a TLE to set the range column */
+		/* Make a TLE to set the range column or start/end columns */
 		result->rangeTargetList = NIL;
-		tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
-		result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+		if (result->startVar)
+		{
+			Expr *boundTLEExpr;
+
+			/* set the start column */
+			boundTLEExpr = (Expr *) makeFuncCall(SystemFuncName("lower"),
+					list_make1(rangeTLEExpr),
+					COERCE_EXPLICIT_CALL,
+					forPortionOf->location);
+			boundTLEExpr = (Expr *) transformExpr(pstate, (Node *) boundTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(boundTLEExpr, start_attno, startcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* set the end column */
+			boundTLEExpr = (Expr *) makeFuncCall(SystemFuncName("upper"),
+					list_make1(rangeTLEExpr),
+					COERCE_EXPLICIT_CALL,
+					forPortionOf->location);
+			boundTLEExpr = (Expr *) transformExpr(pstate, (Node *) boundTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(boundTLEExpr, end_attno, endcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* Mark the start/end columns as requiring update permissions */
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  start_attno - FirstLowInvalidHeapAttributeNumber);
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  end_attno - FirstLowInvalidHeapAttributeNumber);
+		}
+		else
+		{
+			rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+		}
 
 		/* Mark the range column as requiring update permissions */
 		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3bcd0c54216..34a14e52db8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -598,7 +598,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
 
-%type <node>	DomainConstraint TableConstraint TableLikeClause
+%type <node>	DomainConstraint TableConstraint TableLikeClause TablePeriod
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
 %type <list>	ColQualList
@@ -2696,6 +2696,24 @@ alter_table_cmd:
 					n->def = (Node *) $4;
 					$$ = (Node *) n;
 				}
+			/* ALTER TABLE <name> ADD PERIOD FOR <name> (<name>, <name>) */
+			| ADD_P TablePeriod
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_AddPeriod;
+					n->def = $2;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> DROP PERIOD FOR <name> [RESTRICT|CASCADE] */
+			| DROP PERIOD FOR name opt_drop_behavior
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_DropPeriod;
+					n->name = $4;
+					n->behavior = $5;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			/* ALTER TABLE <name> ADD CONSTRAINT ... */
 			| ADD_P TableConstraint
 				{
@@ -3826,8 +3844,10 @@ TableElement:
 			columnDef							{ $$ = $1; }
 			| TableLikeClause					{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
+			| TablePeriod						{ $$ = $1; }
 		;
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4176,6 +4196,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7282,6 +7315,14 @@ CommentStmt:
@@ -17831,7 +17872,6 @@ unreserved_keyword:
 			| PATH
-			| PERIOD
 			| PLAN
@@ -18137,6 +18177,7 @@ reserved_keyword:
index 2f64eaf0e37..75ae0fb8452 100644
@@ -3188,6 +3189,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
@@ -3211,12 +3213,20 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,

+		/* If this column is from a PERIOD, skip it */
+		if (bms_is_member(varnode->varattno, periodatts))
+				continue;
+
 		te = makeTargetEntry((Expr *) varnode,
 							 (AttrNumber) pstate->p_next_resno++,
 							 label,
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 905c986dc6a..f62a2c5b1ee 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -38,6 +38,7 @@
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
@@ -83,6 +84,7 @@ typedef struct
 	bool		isforeign;		/* true if CREATE/ALTER FOREIGN TABLE */
 	bool		isalter;		/* true if altering existing table */
 	List	   *columns;		/* ColumnDef items */
+	List	   *periods;		/* PeriodDef items */
 	List	   *ckconstraints;	/* CHECK constraints */
 	List	   *fkconstraints;	/* FOREIGN KEY constraints */
 	List	   *ixconstraints;	/* index-creating constraints */
@@ -112,6 +114,8 @@ typedef struct
 
@@ -242,6 +246,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	cxt.ixconstraints = NIL;
 
+			case T_PeriodDef:
@@ -342,6 +351,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
 	result = lappend(cxt.blist, stmt);
@@ -870,6 +880,91 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
+	DefElem	   *dcolexists = NULL;
+		else if (strcmp(defel->defname, "colexists") == 0)
+			if (dcolexists)
+			dcolexists = defel;
+	if (dcolexists != NULL)
+		period->colexists = defGetBoolean(dcolexists);
+		period->colexists = false;
+ *		transform a PeriodDef node within CREATE TABLE
@@ -957,6 +1052,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1008,6 +1104,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1017,10 +1114,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
+		 * If this column is from a PERIOD, skip it
+		 * (since LIKE never copies PERIODs).
+		 */
+		if (bms_is_member(parent_attno, periodatts))
+			continue;
+
+		attribute = TupleDescAttr(tupleDesc, parent_attno - 1);
+
 		/*
 		 * Ignore dropped columns in the parent.
 		 */
@@ -2407,6 +2512,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			char	   *key = strVal(lfirst(lc));
 			bool		found = false;
 			bool		forced_not_null = false;
+			bool		hasperiod = false;
 			ColumnDef  *column = NULL;
 			ListCell   *columns;
 			IndexElem  *iparam;
@@ -2425,19 +2531,40 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			if (!found)
 				column = NULL;
 
+			/* If the key is WITHOUT OVERLAPS, a PERIOD will work too. */
+			if (!found && constraint->without_overlaps &&
+				lc == list_last_cell(constraint->keys))
+			{
+				PeriodDef  *period = NULL;
+
+				foreach(columns, cxt->periods)
+				{
+					period = lfirst_node(PeriodDef, columns);
+					if (strcmp(period->periodname, key) == 0)
+					{
+						found = true;
+						hasperiod = true;
+						break;
+					}
+				}
+			}
+
 			if (found)
 			{
-				/*
-				 * column is defined in the new table.  For PRIMARY KEY, we
-				 * can apply the not-null constraint cheaply here ... unless
-				 * the column is marked is_from_type, in which case marking it
-				 * here would be ineffective (see MergeAttributes).
-				 */
-				if (constraint->contype == CONSTR_PRIMARY &&
-					!column->is_from_type)
+				if (column)
 				{
-					column->is_not_null = true;
-					forced_not_null = true;
+					/*
+					 * column is defined in the new table.  For PRIMARY KEY, we
+					 * can apply the not-null constraint cheaply here ... unless
+					 * the column is marked is_from_type, in which case marking it
+					 * here would be ineffective (see MergeAttributes).
+					 */
+					if (constraint->contype == CONSTR_PRIMARY &&
+						!column->is_from_type)
+					{
+						column->is_not_null = true;
+						forced_not_null = true;
+					}
 				}
 			}
 			else if (SystemAttributeByName(key) != NULL)
@@ -2551,12 +2678,18 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 						attname = NameStr(attr->attname);
 						if (strcmp(attname, key) == 0)
 						{
+							found = true;
 							typid = attr->atttypid;
 							break;
 						}
 					}
 				}
-				if (found)
+				/*
+				 * In CREATE TABLE we don't know PERIODs' rangetype yet,
+				 * but we know it will be a range/multirange. So if we
+				 * have a PERIOD then we're safe.
+				 */
+				if (found && !hasperiod)
 				{
 					if (!OidIsValid(typid) && column)
 						typid = typenameTypeId(NULL, column->typeName);
@@ -2907,6 +3040,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 		}
 	}
 
+	/* take care of the period */
+	if (stmt->period)
+		stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
 	/*
 	 * Check that only the base rel is mentioned.  (This should be dead code
 	 * now that add_missing_from is history.)
@@ -3518,6 +3655,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.inhRelations = NIL;
 	cxt.isalter = true;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
@@ -3578,6 +3716,22 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
+					/*
+					 * We can't call transformTablePeriod here
+					 * because it looks at cxt->columns
+					 * and in an ALTER statement the column might already exist
+					 * (or not).
+					 */
+					// TODO: it doesn't look at cxt->columns any more.
+					// So should we call it here? To do what?
+					// Well it might be useful to know about it in cxt->periods at least.
+					transformTablePeriod(&cxt, castNode(PeriodDef, cmd->def));
index edb09d4e356..e137f17e599 100644
index b67c0c03b97..9150cc495f8 100644
@@ -31,6 +31,7 @@
@@ -995,6 +996,68 @@ get_attoptions(Oid relid, int16 attnum)
 	return result;
 }

 /*
@@ -3553,6 +3616,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 68e321212d9..851cf50dd64 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3704,6 +3704,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 			 strcmp(type, "DATABASE PROPERTIES") == 0 ||
 			 strcmp(type, "DEFAULT") == 0 ||
 			 strcmp(type, "FK CONSTRAINT") == 0 ||
+			 strcmp(type, "PERIOD") == 0 ||
 			 strcmp(type, "INDEX") == 0 ||
 			 strcmp(type, "RULE") == 0 ||
 			 strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 67127dbd9fe..6a2474643b7 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6693,6 +6693,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reltype;
 	int			i_relowner;
 	int			i_relchecks;
+	int			i_nperiod;
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
@@ -6770,6 +6771,14 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
+	/* In PG18 upwards we have PERIODs. */
+	if (fout->remoteVersion >= 180000)
+		appendPQExpBufferStr(query,
+							 "(SELECT count(*) FROM pg_period WHERE perrelid = c.oid) AS nperiods, ");
+	else
+		appendPQExpBufferStr(query,
+							 "0 AS nperiods, ");
+
 	if (fout->remoteVersion >= 90300)
 		appendPQExpBufferStr(query,
 							 "c.relispopulated, ");
@@ -6907,6 +6916,7 @@ getTables(Archive *fout, int *numTables)
 	i_reltype = PQfnumber(res, "reltype");
 	i_relowner = PQfnumber(res, "relowner");
 	i_relchecks = PQfnumber(res, "relchecks");
+	i_nperiod = PQfnumber(res, "nperiods");
 	i_relhasindex = PQfnumber(res, "relhasindex");
 	i_relhasrules = PQfnumber(res, "relhasrules");
 	i_relpages = PQfnumber(res, "relpages");
@@ -6990,6 +7000,7 @@ getTables(Archive *fout, int *numTables)
 		}
 		tblinfo[i].reltablespace = pg_strdup(PQgetvalue(res, i, i_reltablespace));
 		tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0);
+		tblinfo[i].nperiod = atoi(PQgetvalue(res, i, i_nperiod));
 		tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0);
 		tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence));
 		tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0);
@@ -7413,7 +7424,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		appendPQExpBufferStr(query,
 							 "false AS indnullsnotdistinct, ");
 
-	if (fout->remoteVersion >= 170000)
+	if (fout->remoteVersion >= 180000)
 		appendPQExpBufferStr(query,
 							 "c.conperiod ");
 	else
@@ -8623,7 +8634,7 @@ getTransforms(Archive *fout)
 /*
  * getTableAttrs -
  *	  for each interesting table, read info about its attributes
- *	  (names, types, default values, CHECK constraints, etc)
+ *	  (names, types, default values, CHECK constraints, PERIODs, etc)
  *
  *	modifies tblinfo
  */
@@ -8673,6 +8684,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	for (int i = 0; i < numTables; i++)
 	{
 		TableInfo  *tbinfo = &tblinfo[i];
+		int			ndumpablechecks;	/* number of CHECK constraints that do
+										   not belong to a period */
 
@@ -8687,7 +8700,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9049,15 +9063,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,
+		if (fout->remoteVersion >= 180000)
+			 * PERIODs were added in v18 and we don't dump CHECK

@@ -9079,6 +9114,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9098,12 +9134,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9162,6 +9199,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+			/* We shouldn't have any periods before v18 */
+			Assert(fout->remoteVersion >= 180000);
+				"JOIN pg_catalog.pg_attribute AS ra ON (ra.attrelid, ra.attnum) = (p.perrelid, p.perrange) "
+				"JOIN pg_catalog.pg_type AS r ON r.oid = ra.atttypid "
@@ -10390,6 +10501,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15942,6 +16055,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
+				/* Always say colexists so we can just print the GENERATED column */
+						"WITH (rangetype = %s, check_constraint_name = %s, colexists = true)",
@@ -15950,7 +16090,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16149,7 +16289,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16442,7 +16582,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18411,6 +18551,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index b25b9f9304e..8187ada66f1 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -59,6 +59,7 @@ typedef enum
 	DO_TRIGGER,
 	DO_CONSTRAINT,
 	DO_FK_CONSTRAINT,			/* see note for ConstraintInfo */
+	DO_PERIOD,
 	DO_PROCLANG,
 	DO_CAST,
 	DO_TABLE_DATA,
@@ -300,12 +301,14 @@ typedef struct _tableInfo
 	bool		rowsec;			/* is row security enabled? */
 	bool		forcerowsec;	/* is row security forced? */
 	bool		hasoids;		/* does it have OIDs? */
+	bool		hasperiods;		/* does it have any periods? */
 	uint32		frozenxid;		/* table's relfrozenxid */
 	uint32		minmxid;		/* table's relminmxid */
 	Oid			toast_oid;		/* toast table's OID, or 0 if none */
 	uint32		toast_frozenxid;	/* toast table's relfrozenxid, if any */
 	uint32		toast_minmxid;	/* toast table's relminmxid */
 	int			ncheck;			/* # of CHECK expressions */
+	int			nperiod;		/* # of PERIOD definitions */
 	Oid			reltype;		/* OID of table's composite type, if any */
 	Oid			reloftype;		/* underlying type for typed table */
 	Oid			foreign_server; /* foreign server oid, if applicable */
@@ -336,6 +339,7 @@ typedef struct _tableInfo
 	char	   *attstorage;		/* attribute storage scheme */
 	char	   *typstorage;		/* type storage scheme */
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
+	bool	   *attisperiod;	/* true if attr is a PERIOD; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
@@ -350,6 +354,7 @@ typedef struct _tableInfo
 	bool	   *inhNotNull;		/* true if NOT NULL is inherited */
 	struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
 	struct _constraintInfo *checkexprs; /* CHECK constraints */
+	struct _periodInfo *periods;	/* PERIOD definitions */
 	bool		needs_override; /* has GENERATED ALWAYS AS IDENTITY */
 	char	   *amname;			/* relation access method */
 
@@ -479,6 +484,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
+typedef struct _periodInfo
+{
+	DumpableObject dobj;
+	TableInfo  *pertable;
+	char	   *perstart;		/* the name of the start column */
+	char	   *perend;			/* the name of the end column */
+	char	   *rngtype;		/* the name of the range type */
+	char	   *conname;		/* the name of the CHECK constraint */
+} PeriodInfo;
+
 typedef struct _procLangInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 4cb754caa55..5602cda4d5c 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -86,6 +86,7 @@ enum dbObjectTypePriorities
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
 	PRIO_INDEX_ATTACH,
+	PRIO_PERIOD,
 	PRIO_STATSEXT,
 	PRIO_RULE,
 	PRIO_TRIGGER,
@@ -121,6 +122,7 @@ static const int dbObjectTypePriority[] =
 	[DO_ATTRDEF] = PRIO_ATTRDEF,
 	[DO_INDEX] = PRIO_INDEX,
 	[DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH,
+	[DO_PERIOD] = PRIO_PERIOD,
 	[DO_STATSEXT] = PRIO_STATSEXT,
 	[DO_RULE] = PRIO_RULE,
 	[DO_TRIGGER] = PRIO_TRIGGER,
@@ -1371,6 +1373,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "FK CONSTRAINT %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_PERIOD:
+			snprintf(buf, bufsize,
+					 "PERIOD %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 		case DO_PROCLANG:
 			snprintf(buf, bufsize,
 					 "PROCEDURAL LANGUAGE %s  (ID %d OID %u)",
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 90b2edb552f..b1fb952a416 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1947,6 +1947,8 @@ describeOneTableDetails(const char *schemaname,
 
+	if (pset.sversion >= 180000)
 
 	res = PSQLexec(buf.data);
@@ -2369,6 +2371,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */
+		if (pset.sversion >= 180000)
+		{
+			printfPQExpBuffer(&buf,
+							  "SELECT quote_ident(p.pername), quote_ident(s.attname) AS startatt, quote_ident(e.attname) AS endatt\n"
+							  "FROM pg_period AS p\n"
+							  "JOIN pg_attribute AS s ON (s.attrelid, s.attnum) = (p.perrelid, p.perstart)\n"
+							  "JOIN pg_attribute AS e ON (e.attrelid, e.attnum) = (p.perrelid, p.perend)\n"
+							  "WHERE p.perrelid = '%s'\n"
+							  "ORDER BY 1;",
+							  oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			if (tuples > 0)
+			{
+				printTableAddFooter(&cont, _("Periods:"));
+				for (i = 0; i < tuples; i++)
+				{
+					/* untranslated constraint name and def */
+					printfPQExpBuffer(&buf, "    %s (%s, %s)",
+									  PQgetvalue(result, i, 0),
+									  PQgetvalue(result, i, 1),
+									  PQgetvalue(result, i, 2));
+
+					printTableAddFooter(&cont, buf.data);
+				}
+			}
+			PQclear(result);
+		}
+
 		/* print indexes */
 		if (tableinfo.hasindex)
 		{
@@ -2383,7 +2419,7 @@ describeOneTableDetails(const char *schemaname,
 			else
 				appendPQExpBufferStr(&buf, ", false AS indisreplident");
 			appendPQExpBufferStr(&buf, ", c2.reltablespace");
-			if (pset.sversion >= 170000)
+			if (pset.sversion >= 180000)
 				appendPQExpBufferStr(&buf, ", con.conperiod");
 			else
 				appendPQExpBufferStr(&buf, ", false AS conperiod");
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 167f91a6e3f..62bb4fc63fb 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -65,6 +65,7 @@ CATALOG_HEADERS := \
 	pg_foreign_server.h \
 	pg_user_mapping.h \
 	pg_foreign_table.h \
+	pg_period.h \
 	pg_policy.h \
 	pg_replication_origin.h \
 	pg_default_acl.h \
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index c512824cd1c..0541821aa43 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -117,6 +117,10 @@ extern List *AddRelationNewConstraints(Relation rel,
 extern void RelationClearMissing(Relation rel);
 extern void SetAttrMissing(Oid relid, char *attname, char *value);
 
index f70d1daba52..d45aa5fa863 100644
index 00000000000..2508cd45a09
@@ -0,0 +1,58 @@
+#include "nodes/bitmapset.h"

 
 /*
index 85cbad3d0c2..628fa53796c 100644

 
 
@@ -106,5 +107,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 										 Oid relId, Oid oldRelId, void *arg);
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
+extern Oid choose_rangetype_for_period(PeriodDef *period);
 
 #endif							/* TABLECMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index cc7bfb0f294..46068765b8c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2306,6 +2306,7 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_PARAMETER_ACL,
+	OBJECT_PERIOD,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
@@ -2394,6 +2395,8 @@ typedef enum AlterTableType
 	AT_ValidateConstraint,		/* validate constraint */
 	AT_AddIndexConstraint,		/* add constraint using existing index */
 	AT_DropConstraint,			/* drop constraint */
+	AT_AddPeriod,				/* ADD PERIOD */
+	AT_DropPeriod,				/* DROP PERIOD */
 	AT_ReAddComment,			/* internal to commands/tablecmds.c */
 	AT_AlterColumnType,			/* alter column type */
 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
@@ -2662,11 +2665,11 @@ typedef struct VariableShowStmt
 /* ----------------------
  *		Create Table Statement
  *
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints is NIL.  After parse analysis,
- * tableElts contains just ColumnDefs, and constraints contains just
- * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present
- * implementation).
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; periods and constraints are NIL.  After parse analysis,
+ * tableElts contains just ColumnDefs, periods contains just PeriodDefs, and
+ * constraints contains just Constraint nodes (in fact, only CONSTR_CHECK nodes,
+ * in the present implementation).
  * ----------------------
  */
 
@@ -2675,6 +2678,7 @@ typedef struct CreateStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to create */
 	List	   *tableElts;		/* column definitions (list of ColumnDef) */
+	List	   *periods;		/* periods (list of PeriodDef nodes) */
 	List	   *inhRelations;	/* relations to inherit from (list of
 								 * RangeVar) */
 	PartitionBoundSpec *partbound;	/* FOR VALUES clause */
@@ -2688,6 +2692,31 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
+ * Definitions for periods in CreateStmt
+ * ----------
+ */
+
+typedef struct PeriodDef
+{
+	NodeTag		type;
+	Oid			oid;			/* period oid, once it's transformed */
+	char	   *periodname;		/* period name */
+	char	   *startcolname;	/* name of start column */
+	char	   *endcolname;		/* name of end column */
+	AttrNumber	startattnum;	/* attnum of the start column */
+	AttrNumber	endattnum;		/* attnum of the end column */
+	AttrNumber	rngattnum;		/* attnum of the GENERATED range column */
+	List	   *options;		/* options from WITH clause */
+	char	   *constraintname;	/* name of the CHECK constraint */
+	char	   *rangetypename;	/* name of the range type */
+	Oid			coltypid;		/* the start/end col type */
+	Oid			rngtypid;		/* the range type to use */
+	bool		colexists;		/* use an existing GENERATED column */
+	int			location;		/* token location, or -1 if unknown */
+} PeriodDef;
+
 /* ----------
  * Definitions for constraints in CreateStmt
  *
@@ -3383,6 +3412,7 @@ typedef struct IndexStmt
 	List	   *indexParams;	/* columns to index: a list of IndexElem */
 	List	   *indexIncludingParams;	/* additional columns to index: a list
 										 * of IndexElem */
+	PeriodDef  *period;			/* The period included in the index */
 	List	   *options;		/* WITH clause options: a list of DefElem */
 	Node	   *whereClause;	/* qualification (partial-index predicate) */
 	List	   *excludeOpNames; /* exclusion operator names, or NIL if none */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 01a101d3776..719cd64a945 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2350,6 +2350,8 @@ typedef struct ForPortionOfExpr
 {
 	NodeTag		type;
 	Var		   *rangeVar;			/* Range column */
+	Var		   *startVar;			/* Start column if PERIOD */
+	Var		   *endVar;				/* End column if PERIOD */
 	char	   *range_name;			/* Range name */
 	Node	   *targetRange;		/* FOR PORTION OF bounds as a range */
 	Oid			rangeType;			/* type of targetRange */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 339f094a80d..7aff4f8dba4 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -339,7 +339,7 @@ PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
-PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 14065894779..d84d02b0280 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -40,5 +40,6 @@ extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel,
 										  Relation source_idx,
 										  const struct AttrMap *attmap,
 										  Oid *constraintOid);
+extern void transformPeriodOptions(PeriodDef *period);
 
index e7ad513146a..acb0649364f 100644
@@ -199,6 +201,7 @@ extern Oid	get_range_subtype(Oid rangeOid);
index 00000000000..d02c17036fd
@@ -0,0 +1,21 @@
index 67ff2b63675..a6fd984dc78 100644
index 5c0a5cf4b1b..ebc43f55f32 100644
--- a/src/test/regress/expected/for_portion_of.out
@@ -532,6 +532,44 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
  [3,4) | {[01-01-2018,)}                                                           | three
 (6 rows)
 
+DROP TABLE for_portion_of_test2;
+-- Test with PERIODs
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_from date,
+  valid_til date,
+  name text NOT NULL,
+  PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2
+VALUES
+('[1,2)', '2018-01-02', '2018-02-03', 'one'),
+('[1,2)', '2018-02-04', '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', null, '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_from | valid_til  | name  
+-------+------------+------------+-------
+ [1,2) | 01-02-2018 | 01-10-2018 | one
+ [1,2) | 01-10-2018 | 02-03-2018 | one^1
+ [1,2) | 02-04-2018 | 02-10-2018 | one^1
+ [1,2) | 02-10-2018 | 03-03-2018 | one
+ [1,2) | 03-03-2018 | 04-04-2018 | one
+ [2,3) | 01-01-2018 | 01-15-2018 | two
+ [2,3) | 02-15-2018 | 05-01-2018 | two
+ [3,4) | 01-01-2018 |            | three
+(8 rows)
+
 DROP TABLE for_portion_of_test2;
 -- Test with a custom range type
 CREATE TYPE mydaterange AS range(subtype=date);
diff --git a/src/test/regress/expected/generated.out b/src/test/regress/expected/generated.out
index 44058db7c1d..0cf8a3a5c80 100644
--- a/src/test/regress/expected/generated.out
+++ b/src/test/regress/expected/generated.out
@@ -19,7 +19,9 @@ SELECT table_name, column_name, dependent_column FROM information_schema.column_
  table_name | column_name | dependent_column 
 ------------+-------------+------------------
  gtest1     | a           | b
-(1 row)
+ pt         | de          | p
+ pt         | ds          | p
+(3 rows)
 
 \d gtest1
                             Table "public.gtest1"
diff --git a/src/test/regress/expected/periods.out b/src/test/regress/expected/periods.out
new file mode 100644
index 00000000000..efd3a846ab9
--- /dev/null
+++ b/src/test/regress/expected/periods.out
@@ -0,0 +1,267 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+ERROR:  PERIOD FOR SYSTEM_TIME is not supported
+LINE 2: create table pt (id integer, ds date, de date, period for sy...
+                                                       ^
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+ERROR:  column "bogus" of relation "pt" does not exist
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+ERROR:  column "bogus" of relation "pt" does not exist
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+ERROR:  start and end columns of period must be of same type
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+ERROR:  start and end columns of period must have same collation
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+ERROR:  no range type for xml found for period p
+HINT:  You can define a custom range type with CREATE TYPE
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+ERROR:  period name "ctid" conflicts with a system column name
+create table pt (id integer, ds date, de date, period for id (ds, de));
+ERROR:  period name "id" conflicts with a column name
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+ERROR:  period name "p" specified more than once
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+ id |     ds     |     de     
+----+------------+------------
+  1 | 01-01-2000 | 01-01-2001
+(1 row)
+
+/* You can get it if you want */
+select *, p from pt;
+ id |     ds     |     de     |            p            
+----+------------+------------+-------------------------
+  1 | 01-01-2000 | 01-01-2001 | [01-01-2000,01-01-2001)
+(1 row)
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+/* Skip creating GENERATED column: works */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Skip creating GENERATED column: fails because the col isn't there */
+create table pt2 (id integer, ds date, de date, period for p (ds, de) with (colexists = true));
+ERROR:  No column found with name p
+/* Skip creating GENERATED column: fails because the option has an invalid value */
+create table pt2 (id integer, ds date, de date, period for p (ds, de) with (colexists = 'whatever'));
+ERROR:  colexists requires a Boolean value
+/* Skip creating GENERATED column: fails because the column is not NOT NULL */
+create table pt2 (id integer, ds date, de date, p daterange generated always as (daterange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+ERROR:  Period p uses a generated column that allows nulls
+/* Skip creating GENERATED column: fails because the column is not GENERATED */
+create table pt2 (id integer, ds date, de date, p daterange not null, period for p (ds, de) with (colexists = true));
+ERROR:  Period p uses a non-generated column
+/* Skip creating GENERATED column: fails because the column is GENERATED but with the wrong expression */
+-- TODO:
+-- create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(de, ds)) stored, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the column is the wrong type */
+create table pt2 (id integer, ds date, de date, p tsrange not null generated always as (tsrange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+ERROR:  Period p uses a generated column with the wrong type
+/* Skip creating GENERATED column: fails because the column is inherited */
+create table pt2parent (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+create table pt2 (period for p (ds, de) with (colexists = true)) inherits (pt2parent);
+ERROR:  Inheriting is not supported when a table has a PERIOD
+drop table pt2parent;
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+ERROR:  PERIOD FOR SYSTEM_TIME is not supported
+alter table pt add period for p (ds, de);
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+/* Can't drop its columns */
+alter table pt drop column ds;
+ERROR:  cannot drop column ds of table pt because other objects depend on it
+DETAIL:  period p on table pt depends on column ds of table pt
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+alter table pt drop column de;
+ERROR:  cannot drop column de of table pt because other objects depend on it
+DETAIL:  period p on table pt depends on column de of table pt
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+ERROR:  cannot alter type of a column used by a period
+DETAIL:  period p on table pt depends on column "ds"
+alter table pt alter column ds type timestamp;
+ERROR:  cannot alter type of a column used by a period
+DETAIL:  period p on table pt depends on column "ds"
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+ERROR:  column name "p" conflicts with a period name
+alter table pt rename column id to p;
+ERROR:  column name "p" conflicts with a period name
+alter table pt add period for tableoid (ds, de);
+ERROR:  period name "tableoid" conflicts with a system column name
+alter table pt add period for "........pg.dropped.4........" (ds, de);
+ERROR:  period name "........pg.dropped.4........" conflicts with a column name
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+ERROR:  ambiguous range for type date
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+ERROR:  cannot drop type mydaterange because other objects depend on it
+DETAIL:  period p on table pt2 depends on type mydaterange
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+drop type mydaterange cascade;
+NOTICE:  drop cascades to period p on table pt2
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+ERROR:  Range type notarange not found
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+ERROR:  Range type tstzrange does not match column type date
+drop table pt2;
+/* Skip creating GENERATED column: works */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Skip creating GENERATED column: fails because the col isn't there */
+create table pt2 (id integer, ds date, de date);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+ERROR:  column "p" of relation "pt2" does not exist
+drop table pt2;
+/* Skip creating GENERATED column: fails because the option has an invalid value */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = 'whatever');
+ERROR:  colexists requires a Boolean value
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is not NOT NULL */
+create table pt2 (id integer, ds date, de date, p daterange generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+ERROR:  Period p uses a generated column that allows nulls
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is not GENERATED */
+create table pt2 (id integer, ds date, de date, p daterange not null);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+ERROR:  Period p uses a non-generated column
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is GENERATED but with the wrong expression */
+-- TODO:
+-- create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(de, ds)) stored);
+-- alter table pt2 add period for p (ds, de) with (colexists = true);
+/* Skip creating GENERATED column: fails because the column is the wrong type */
+create table pt2 (id integer, ds date, de date, p tsrange not null generated always as (tsrange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+ERROR:  Period p uses a generated column with the wrong type
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is inherited */
+create table pt2parent (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+create table pt2 () inherits (pt2parent);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+ERROR:  Period p uses a generated column that is inherited
+drop table pt2;
+drop table pt2parent;
+/* CREATE TABLE (LIKE ...) */
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+
+drop table pt2;
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p (ds, de)
+Check constraints:
+    "pt2_p_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+                Table "public.pt2"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ id     | integer |           |          | 
+ ds     | date    |           |          | 
+ de     | date    |           |          | 
+Periods:
+    p2 (ds, de)
+Check constraints:
+    "pt2_p2_check" CHECK (ds < de)
+
+drop table pt2;
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+ERROR:  period name "id" conflicts with a column name
+/* CREATE TALBE INHERITS */
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+ERROR:  Inheriting from a table with a PERIOD is not supported
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+ERROR:  Inheriting is not supported when a table has a PERIOD
+drop table not_p;
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index bbd222db361..72f8783b476 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -153,6 +153,76 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
  CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at)
 (1 row)
 
+-- PK with one column plus a PERIOD:
+CREATE TABLE temporal_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per
+               Table "public.temporal_per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_per_valid_at_check" CHECK (valid_from < valid_til)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per_pk';
+            pg_get_constraintdef             
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per_pk';
+                                pg_get_indexdef                                
+-------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_per_pk ON temporal_per USING gist (id, valid_at)
+(1 row)
+
+-- PK with two columns plus a PERIOD:
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per2
+              Table "public.temporal_per2"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id1        | int4range |           | not null | 
+ id2        | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_per2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_per2_valid_at_check" CHECK (valid_from < valid_til)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per2_pk';
+               pg_get_constraintdef                
+---------------------------------------------------
+ PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per2_pk';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_per2_pk ON temporal_per2 USING gist (id1, id2, valid_at)
+(1 row)
+
 -- PK with a custom range type:
 CREATE TYPE textrange2 AS range (subtype=text, collation="C");
 CREATE TABLE temporal_rng3 (
@@ -300,6 +370,78 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
 (1 row)
 
 DROP TABLE temporal_rng3;
+-- UNIQUE with one column plus a PERIOD:
+CREATE TABLE temporal_per3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per3
+              Table "public.temporal_per3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           |          | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_per3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_per3_valid_at_check" CHECK (valid_from < valid_til)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+          pg_get_constraintdef          
+----------------------------------------
+ UNIQUE (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+                                 pg_get_indexdef                                 
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_per3_uq ON temporal_per3 USING gist (id, valid_at)
+(1 row)
+
+DROP TABLE temporal_per3;
+-- UNIQUE with two columns plus a PERIOD:
+CREATE TABLE temporal_per3 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per3
+              Table "public.temporal_per3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id1        | int4range |           |          | 
+ id2        | int4range |           |          | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_per3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_per3_valid_at_check" CHECK (valid_from < valid_til)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+             pg_get_constraintdef             
+----------------------------------------------
+ UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+                                    pg_get_indexdef                                    
+---------------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_per3_uq ON temporal_per3 USING gist (id1, id2, valid_at)
+(1 row)
+
+DROP TABLE temporal_per3;
 -- UNIQUE with a custom range type:
 CREATE TYPE textrange2 AS range (subtype=text, collation="C");
 CREATE TABLE temporal_rng3 (
@@ -377,6 +519,106 @@ ALTER TABLE temporal3
 	ADD CONSTRAINT temporal3_uq
 	UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DROP TABLE temporal3;
+-- PRIMARY KEY with PERIOD already there
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal3
+  ADD CONSTRAINT temporal3_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+                Table "public.temporal3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal3_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal3_valid_at_check" CHECK (valid_from < valid_til)
+
+DROP TABLE temporal3;
+-- PRIMARY KEY with PERIOD too
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date
+);
+ALTER TABLE temporal3
+  ADD PERIOD FOR valid_at (valid_from, valid_til),
+  ADD CONSTRAINT temporal3_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+                Table "public.temporal3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal3_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal3_valid_at_check" CHECK (valid_from < valid_til)
+
+DROP TABLE temporal3;
+-- UNIQUE with PERIOD already there
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal3
+  ADD CONSTRAINT temporal3_uq
+  UNIQUE (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+                Table "public.temporal3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           |          | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal3_valid_at_check" CHECK (valid_from < valid_til)
+
+DROP TABLE temporal3;
+-- UNIQUE with PERIOD too
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date
+);
+ALTER TABLE temporal3
+  ADD PERIOD FOR valid_at (valid_from, valid_til),
+  ADD CONSTRAINT temporal3_uq
+  UNIQUE (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+                Table "public.temporal3"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           |          | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal3_valid_at_check" CHECK (valid_from < valid_til)
+
+DROP TABLE temporal3;
 --
 -- range PK: test with existing rows
 --
@@ -874,6 +1116,284 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
 DROP TABLE temporal_mltrng3;
 --
+-- PERIOD PK: test with existing rows
+--
+ALTER TABLE temporal_per DROP CONSTRAINT temporal_per_pk;
+-- okay:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_per DROP CONSTRAINT temporal_per_pk;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+  ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_per_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
+ROLLBACK;
+-- rejects empty:
+BEGIN;
+  ALTER TABLE temporal_per DROP PERIOD FOR valid_at;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+  ALTER TABLE temporal_per ADD PERIOD FOR valid_at (valid_from, valid_til);
+ERROR:  check constraint "temporal_per_valid_at_check" of relation "temporal_per" is violated by some row
+ROLLBACK;
+ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_per;
+--
+-- PERIOD PK: test inserts
+--
+-- okay:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+-- should fail:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+ERROR:  conflicting key value violates exclusion constraint "temporal_per_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+ERROR:  null value in column "id" of relation "temporal_per" violates not-null constraint
+DETAIL:  Failing row contains (null, 2018-01-01, 2018-01-05, [2018-01-01,2018-01-05)).
+-- rejects empty:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+ERROR:  new row for relation "temporal_per" violates check constraint "temporal_per_valid_at_check"
+DETAIL:  Failing row contains ([3,4), 2020-01-01, 2020-01-01, empty).
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2018-01-02 | 2018-02-03
+ [1,2) | 2018-03-03 | 2018-04-04
+ [2,3) | 2018-01-01 | 2018-01-05
+ [3,4) | 2018-01-01 | 
+(4 rows)
+
+--
+-- PERIOD PK: test updates
+--
+-- update the scalar part
+UPDATE  temporal_per
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the PERIOD bounds
+UPDATE  temporal_per
+SET     valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_per
+SET     id = '[21,22)',
+        valid_from = '2018-01-02',
+        valid_til = '2018-02-03'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+   id    | valid_from | valid_til  
+---------+------------+------------
+ [1,2)   | 2018-03-03 | 2018-04-04
+ [2,3)   | 2018-01-01 | 2018-01-05
+ [3,4)   | 2018-01-01 | 
+ [21,22) | 2018-01-02 | 2018-02-03
+(4 rows)
+
+-- should fail:
+UPDATE  temporal_per
+SET     id = '[1,2)',
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+ERROR:  conflicting key value violates exclusion constraint "temporal_per_pk"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-03-05,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+-- set the scalar part to NULL
+UPDATE  temporal_per
+SET     id = NULL,
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+ERROR:  null value in column "id" of relation "temporal_per" violates not-null constraint
+DETAIL:  Failing row contains (null, 2018-03-05, 2018-05-05, [2018-03-05,2018-05-05)).
+-- set a PERIOD bound to NULL (okay)
+UPDATE  temporal_per
+SET     id = '[1,2)',
+        valid_from = '2018-04-04',
+        valid_til = NULL
+WHERE   id = '[21,22)';
+-- rejects empty:
+UPDATE  temporal_per
+SET     id = '[21,22)',
+        valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)';
+ERROR:  new row for relation "temporal_per" violates check constraint "temporal_per_valid_at_check"
+DETAIL:  Failing row contains ([21,22), 2020-01-01, 2020-01-01, empty).
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2018-03-03 | 2018-04-04
+ [1,2) | 2018-04-04 | 
+ [2,3) | 2018-01-01 | 2018-01-05
+ [3,4) | 2018-01-01 | 
+(4 rows)
+
+--
+-- PERIOD UQ: test with existing rows
+--
+CREATE TABLE temporal_per3 (
+	id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+-- okay:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_per3 DROP CONSTRAINT temporal_per3_uq;
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+  ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ERROR:  could not create exclusion constraint "temporal_per3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)) conflicts with key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)).
+ROLLBACK;
+-- rejects empty:
+BEGIN;
+  ALTER TABLE temporal_per3 DROP PERIOD FOR valid_at;
+  INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+  ALTER TABLE temporal_per3 ADD PERIOD FOR valid_at (valid_from, valid_til);
+ERROR:  check constraint "temporal_per3_valid_at_check" of relation "temporal_per3" is violated by some row
+ROLLBACK;
+ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_per3;
+--
+-- PERIOD UQ: test inserts
+--
+-- okay:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+-- should fail:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+ERROR:  conflicting key value violates exclusion constraint "temporal_per3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-01-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-01-02,2018-02-03)).
+-- rejects empty:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+ERROR:  new row for relation "temporal_per3" violates check constraint "temporal_per3_valid_at_check"
+DETAIL:  Failing row contains ([3,4), 2020-01-01, 2020-01-01, empty).
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2018-01-02 | 2018-02-03
+ [1,2) | 2018-03-03 | 2018-04-04
+ [2,3) | 2018-01-01 | 2018-01-05
+ [3,4) | 2018-01-01 | 
+       | 2018-01-01 | 2018-01-05
+(5 rows)
+
+--
+-- PERIOD UQ: test updates
+--
+-- update the scalar part
+UPDATE  temporal_per3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the PERIOD bounds
+UPDATE  temporal_per3
+SET     valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_per3
+SET     id = '[21,22)',
+        valid_from = '2018-01-02',
+        valid_til = '2018-02-03'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+   id    | valid_from | valid_til  
+---------+------------+------------
+ [1,2)   | 2018-03-03 | 2018-04-04
+ [2,3)   | 2018-01-01 | 2018-01-05
+ [3,4)   | 2018-01-01 | 
+ [21,22) | 2018-01-02 | 2018-02-03
+         | 2018-01-01 | 2018-01-05
+(5 rows)
+
+-- should fail:
+UPDATE  temporal_per3
+SET     id = '[1,2)',
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+ERROR:  conflicting key value violates exclusion constraint "temporal_per3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-03-05,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+-- set the scalar part to NULL (okay)
+UPDATE  temporal_per3
+SET     id = NULL,
+        valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[21,22)';
+-- set a PERIOD bound to NULL (okay)
+UPDATE  temporal_per3
+SET     id = '[1,2)',
+        valid_from = '2018-04-04',
+        valid_til = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2018-03-03 | 2018-04-04
+ [1,2) | 2018-04-04 | 
+ [2,3) | 2018-01-01 | 2018-01-05
+ [3,4) | 2018-01-01 | 
+       | 2018-01-01 | 2018-01-05
+(5 rows)
+
+-- should fail:
+UPDATE  temporal_per3
+SET     valid_from = '2018-03-01',
+        valid_til = '2018-05-05'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+ERROR:  conflicting key value violates exclusion constraint "temporal_per3_uq"
+DETAIL:  Key (id, valid_at)=([1,2), [2018-03-01,2018-05-05)) conflicts with existing key (id, valid_at)=([1,2), [2018-03-03,2018-04-04)).
+-- rejects empty:
+UPDATE  temporal_per3
+SET     valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+ERROR:  new row for relation "temporal_per3" violates check constraint "temporal_per3_valid_at_check"
+DETAIL:  Failing row contains ([1,2), 2020-01-01, 2020-01-01, empty).
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_per3
+SET     id = NULL,
+        valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+ERROR:  new row for relation "temporal_per3" violates check constraint "temporal_per3_valid_at_check"
+DETAIL:  Failing row contains (null, 2020-01-01, 2020-01-01, empty).
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2018-03-03 | 2018-04-04
+ [1,2) | 2018-04-04 | 
+ [2,3) | 2018-01-01 | 2018-01-05
+ [3,4) | 2018-01-01 | 
+       | 2018-01-01 | 2018-01-05
+(5 rows)
+
+DROP TABLE temporal_per3;
+--
 -- test a range with both a PK and a UNIQUE constraint
 --
 CREATE TABLE temporal3 (
@@ -1301,6 +1821,18 @@ CREATE TABLE temporal_fk_rng2rng (
 	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng (id, PERIOD valid_at)
 );
+\d temporal_fk_rng2rng
+           Table "public.temporal_fk_rng2rng"
+  Column   |   Type    | Collation | Nullable | Default 
+-----------+-----------+-----------+----------+---------
+ id        | int4range |           | not null | 
+ valid_at  | daterange |           | not null | 
+ parent_id | int4range |           |          | 
+Indexes:
+    "temporal_fk_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk_rng2rng_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+
 DROP TABLE temporal_fk_rng2rng;
 -- with mismatched PERIOD columns:
 -- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
@@ -3503,6 +4035,1240 @@ SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, val
  [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
 (2 rows)
 
+--
+-- test FOREIGN KEY, PERIOD references PERIOD
+--
+-- test table setup
+DROP TABLE temporal_per;
+CREATE TABLE temporal_per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal_per
+	ADD CONSTRAINT temporal_per_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from int,
+	valid_til int,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_per2per_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4range and daterange.
+-- works: PERIOD for both referenced and referencing
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_per2per
+           Table "public.temporal_fk_per2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id  | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk_per2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk_per2per_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk_per2per_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_per(id, PERIOD valid_at)
+
+DROP TABLE temporal_fk_per2per;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_per2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_per (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_per2;
+CREATE TABLE temporal_per2 (
+	id1 int4range,
+	id2 int4range,
+	valid_from date,
+	valid_til date,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_per2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2per
+           Table "public.temporal_fk2_per2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2per_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_per2per;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2per
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2per
+           Table "public.temporal_fk2_per2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2per_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	DROP PERIOD FOR valid_at,
+	ALTER COLUMN valid_from TYPE timestamp,
+	ALTER COLUMN valid_til TYPE timestamp,
+	ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+ERROR:  foreign key constraint "temporal_fk_per2per_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsrange and daterange.
+ALTER TABLE temporal_fk_per2per
+	DROP PERIOD FOR valid_at,
+	ALTER COLUMN valid_from TYPE date,
+	ALTER COLUMN valid_til TYPE date,
+	ADD PERIOD FOR valid_at (valid_from, valid_til);
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_per (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_per2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- okay again:
+DELETE FROM temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2per_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_per(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK referencing inserts
+--
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+--
+-- test FK referencing updates
+--
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-05-01)) is not present in table "temporal_per".
+UPDATE temporal_fk_per2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-03-01)) is not present in table "temporal_per".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+--
+-- test FK parent updates NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK parent updates RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK parent deletes NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK parent deletes RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- per2per test ON UPDATE/DELETE options
+--
+-- TOC:
+-- parent updates CASCADE
+-- parent deletes CASCADE
+-- parent updates SET NULL
+-- parent deletes SET NULL
+-- parent updates SET DEFAULT
+-- parent deletes SET DEFAULT
+-- parent updates CASCADE (two scalar cols)
+-- parent deletes CASCADE (two scalar cols)
+-- parent updates SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols, SET NULL subset)
+-- parent updates SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+-- test FK parent updates CASCADE
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes CASCADE
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id 
+----+------------+-----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | parent_id 
+-----------+------------+------------+-----------
+ [200,201) | 2020-01-01 | 2021-01-01 | [8,9)
+(1 row)
+
+--
+-- test FK parent updates SET NULL
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | parent_id 
+-----------+------------+------------+-----------
+ [200,201) | 2018-01-01 | 2020-01-01 | 
+ [200,201) | 2020-01-01 | 2021-01-01 | [8,9)
+(2 rows)
+
+--
+-- test FK parent deletes SET NULL
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | parent_id 
+-----------+------------+------------+-----------
+ [200,201) | 2018-01-01 | 2020-01-01 | 
+ [200,201) | 2020-01-01 | 2021-01-01 | [8,9)
+(2 rows)
+
+--
+-- test FK parent updates SET DEFAULT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes SET DEFAULT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent updates CASCADE (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes CASCADE (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id1 | parent_id2 
+----+------------+-----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | parent_id1 | parent_id2 
+-----------+------------+------------+------------+------------
+ [200,201) | 2020-01-01 | 2021-01-01 | [8,9)      | [8,9)
+(1 row)
+
+--
+-- test FK parent updates SET NULL (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes SET NULL (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes SET NULL (two scalar cols, SET NULL subset)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		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_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent updates SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes SET DEFAULT (two scalar cols)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		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_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 (
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5e9693a1d24..d9225cf3685 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -43,7 +43,7 @@ test: copy copyselect copydml insert insert_conflict
 # Note: many of the tests in later groups depend on create_index
 # ----------
 test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist periods
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index e802b5c67ff..e25215a3fd5 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -423,6 +423,38 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
 
 DROP TABLE for_portion_of_test2;
 
+-- Test with PERIODs
+
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_from date,
+  valid_til date,
+  name text NOT NULL,
+  PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2
+VALUES
+('[1,2)', '2018-01-02', '2018-02-03', 'one'),
+('[1,2)', '2018-02-04', '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', null, '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;
+
 -- Test with a custom range type
 
 CREATE TYPE mydaterange AS range(subtype=date);
diff --git a/src/test/regress/sql/periods.sql b/src/test/regress/sql/periods.sql
new file mode 100644
index 00000000000..5d39712d2b4
--- /dev/null
+++ b/src/test/regress/sql/periods.sql
@@ -0,0 +1,178 @@
+/* System periods are not implemented */
+create table pt (id integer, ds date, de date, period for system_time (ds, de));
+
+/* Periods must specify actual columns */
+create table pt (id integer, ds date, de date, period for p (bogus, de));
+create table pt (id integer, ds date, de date, period for p (ds, bogus));
+
+/* Data types must match exactly */
+create table pt (id integer, ds date, de timestamp, period for p (ds, de));
+create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de));
+
+/* Periods must have a default BTree operator class */
+create table pt (id integer, ds xml, de xml, period for p (ds, de));
+
+/* Period and column names are in the same namespace */
+create table pt (id integer, ds date, de date, period for ctid (ds, de));
+create table pt (id integer, ds date, de date, period for id (ds, de));
+
+/* Period name can't be given more than once */
+create table pt (id integer, ds date, de date, period for p (ds, de), period for p (ds, de));
+
+/* Now make one that works */
+create table pt (id integer, ds date, de date, period for p (ds, de));
+
+/* SELECT * excludes the PERIOD */
+insert into pt values (1, '2000-01-01', '2001-01-01');
+select * from pt;
+
+/* You can get it if you want */
+select *, p from pt;
+
+/* Two are okay */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de), period for p2 (ds, de));
+drop table pt2;
+
+/* Skip creating GENERATED column: works */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+\d pt2
+drop table pt2;
+/* Skip creating GENERATED column: fails because the col isn't there */
+create table pt2 (id integer, ds date, de date, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the option has an invalid value */
+create table pt2 (id integer, ds date, de date, period for p (ds, de) with (colexists = 'whatever'));
+/* Skip creating GENERATED column: fails because the column is not NOT NULL */
+create table pt2 (id integer, ds date, de date, p daterange generated always as (daterange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the column is not GENERATED */
+create table pt2 (id integer, ds date, de date, p daterange not null, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the column is GENERATED but with the wrong expression */
+-- TODO:
+-- create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(de, ds)) stored, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the column is the wrong type */
+create table pt2 (id integer, ds date, de date, p tsrange not null generated always as (tsrange(ds, de)) stored, period for p (ds, de) with (colexists = true));
+/* Skip creating GENERATED column: fails because the column is inherited */
+create table pt2parent (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+create table pt2 (period for p (ds, de) with (colexists = true)) inherits (pt2parent);
+drop table pt2parent;
+
+
+/*
+ * ALTER TABLE tests
+ */
+alter table pt drop period for p;
+alter table pt add period for system_time (ds, de);
+alter table pt add period for p (ds, de);
+
+/* Adding a second one */
+create table pt2 (id integer, ds date, de date, period for p1 (ds, de));
+alter table pt2 add period for p2 (ds, de);
+drop table pt2;
+
+/* Can't drop its columns */
+alter table pt drop column ds;
+alter table pt drop column de;
+
+/* Can't change the data types */
+alter table pt alter column ds type timestamp;
+alter table pt alter column ds type timestamp;
+
+/* column/period namespace conflicts */
+alter table pt add column p integer;
+alter table pt rename column id to p;
+alter table pt add period for tableoid (ds, de);
+alter table pt add period for "........pg.dropped.4........" (ds, de);
+
+/* adding columns and the period at the same time */
+create table pt2 (id integer);
+alter table pt2 add column ds date, add column de date, add period for p (ds, de);
+drop table pt2;
+
+/* Ambiguous range types raise an error */
+create type mydaterange as range(subtype=date);
+create table pt2 (id int, ds date, de date, period for p (ds, de));
+
+/* You can give an explicit range type */
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange'));
+drop type mydaterange;
+drop type mydaterange cascade;
+drop table pt2;
+create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange'));
+
+/* Range type is not found */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange'));
+
+/* Range type is the wrong type */
+create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange'));
+drop table pt2;
+
+/* Skip creating GENERATED column: works */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+\d pt2
+drop table pt2;
+/* Skip creating GENERATED column: fails because the col isn't there */
+create table pt2 (id integer, ds date, de date);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+drop table pt2;
+/* Skip creating GENERATED column: fails because the option has an invalid value */
+create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = 'whatever');
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is not NOT NULL */
+create table pt2 (id integer, ds date, de date, p daterange generated always as (daterange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is not GENERATED */
+create table pt2 (id integer, ds date, de date, p daterange not null);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is GENERATED but with the wrong expression */
+-- TODO:
+-- create table pt2 (id integer, ds date, de date, p daterange not null generated always as (daterange(de, ds)) stored);
+-- alter table pt2 add period for p (ds, de) with (colexists = true);
+/* Skip creating GENERATED column: fails because the column is the wrong type */
+create table pt2 (id integer, ds date, de date, p tsrange not null generated always as (tsrange(ds, de)) stored);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+drop table pt2;
+/* Skip creating GENERATED column: fails because the column is inherited */
+create table pt2parent (id integer, ds date, de date, p daterange not null generated always as (daterange(ds, de)) stored);
+create table pt2 () inherits (pt2parent);
+alter table pt2 add period for p (ds, de) with (colexists = true);
+drop table pt2;
+drop table pt2parent;
+
+/* CREATE TABLE (LIKE ...) */
+
+/* Periods are not copied by LIKE, so their columns aren't either */
+create table pt2 (like pt);
+\d pt2
+drop table pt2;
+
+/* Can add a period referring to LIKE'd columns */
+create table not_p (id integer, ds date, de date);
+create table pt2 (like not_p, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with the same name */
+create table pt2 (like pt, period for p (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can add a period with a different name */
+create table pt2 (like pt, period for p2 (ds, de));
+\d pt2
+drop table pt2;
+
+/* Can't add a period whose name conflicts with a LIKE'd column */
+create table pt2 (like pt, period for id (ds, de));
+
+/* CREATE TALBE INHERITS */
+
+/* Can't inherit from a table with a period */
+create table pt2 (name text) inherits (pt);
+
+/* Can't inherit with a period */
+create table pt2 (d2s date, d2e date, period for p (d2s, d2e)) inherits (not_p);
+
+drop table not_p;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index c8241c27929..458c45263b7 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -91,6 +91,31 @@ CREATE TABLE temporal_rng2 (
 SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
 SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
 
+-- PK with one column plus a PERIOD:
+CREATE TABLE temporal_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per_pk';
+
+-- PK with two columns plus a PERIOD:
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per2_pk';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per2_pk';
+
 -- PK with a custom range type:
 CREATE TYPE textrange2 AS range (subtype=text, collation="C");
 CREATE TABLE temporal_rng3 (
@@ -169,6 +194,33 @@ SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rn
 SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq';
 DROP TABLE temporal_rng3;
 
+-- UNIQUE with one column plus a PERIOD:
+CREATE TABLE temporal_per3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+DROP TABLE temporal_per3;
+
+-- UNIQUE with two columns plus a PERIOD:
+CREATE TABLE temporal_per3 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_per3
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_per3_uq';
+DROP TABLE temporal_per3;
+
 -- UNIQUE with a custom range type:
 CREATE TYPE textrange2 AS range (subtype=text, collation="C");
 CREATE TABLE temporal_rng3 (
@@ -245,6 +297,58 @@ ALTER TABLE temporal3
 	UNIQUE (id, valid_at WITHOUT OVERLAPS);
 DROP TABLE temporal3;
 
+-- PRIMARY KEY with PERIOD already there
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal3
+  ADD CONSTRAINT temporal3_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+DROP TABLE temporal3;
+
+-- PRIMARY KEY with PERIOD too
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date
+);
+ALTER TABLE temporal3
+  ADD PERIOD FOR valid_at (valid_from, valid_til),
+  ADD CONSTRAINT temporal3_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+DROP TABLE temporal3;
+
+-- UNIQUE with PERIOD already there
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal3
+  ADD CONSTRAINT temporal3_uq
+  UNIQUE (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+DROP TABLE temporal3;
+
+-- UNIQUE with PERIOD too
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date
+);
+ALTER TABLE temporal3
+  ADD PERIOD FOR valid_at (valid_from, valid_til),
+  ADD CONSTRAINT temporal3_uq
+  UNIQUE (id, valid_at WITHOUT OVERLAPS);
+\d temporal3
+DROP TABLE temporal3;
+
 --
 -- range PK: test with existing rows
 --
@@ -615,6 +719,212 @@ WHERE   id = '[1,2)' AND valid_at IS NULL;
 SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 DROP TABLE temporal_mltrng3;
 
+--
+-- PERIOD PK: test with existing rows
+--
+
+ALTER TABLE temporal_per DROP CONSTRAINT temporal_per_pk;
+
+-- okay:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_per DROP CONSTRAINT temporal_per_pk;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+  ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+-- rejects empty:
+BEGIN;
+  ALTER TABLE temporal_per DROP PERIOD FOR valid_at;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+  ALTER TABLE temporal_per ADD PERIOD FOR valid_at (valid_from, valid_til);
+ROLLBACK;
+ALTER TABLE temporal_per ADD CONSTRAINT temporal_per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_per;
+
+--
+-- PERIOD PK: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+
+-- should fail:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+-- rejects empty:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+
+--
+-- PERIOD PK: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_per
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the PERIOD bounds
+UPDATE  temporal_per
+SET     valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_per
+SET     id = '[21,22)',
+        valid_from = '2018-01-02',
+        valid_til = '2018-02-03'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_per
+SET     id = '[1,2)',
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+-- set the scalar part to NULL
+UPDATE  temporal_per
+SET     id = NULL,
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+-- set a PERIOD bound to NULL (okay)
+UPDATE  temporal_per
+SET     id = '[1,2)',
+        valid_from = '2018-04-04',
+        valid_til = NULL
+WHERE   id = '[21,22)';
+-- rejects empty:
+UPDATE  temporal_per
+SET     id = '[21,22)',
+        valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)';
+SELECT * FROM temporal_per ORDER BY id, valid_at;
+
+--
+-- PERIOD UQ: test with existing rows
+--
+
+CREATE TABLE temporal_per3 (
+	id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+
+-- okay:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ALTER TABLE temporal_per3 DROP CONSTRAINT temporal_per3_uq;
+
+-- should fail:
+BEGIN;
+  INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+  ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+ROLLBACK;
+-- rejects empty:
+BEGIN;
+  ALTER TABLE temporal_per3 DROP PERIOD FOR valid_at;
+  INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+  ALTER TABLE temporal_per3 ADD PERIOD FOR valid_at (valid_from, valid_til);
+ROLLBACK;
+ALTER TABLE temporal_per3 ADD CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS);
+DELETE FROM temporal_per3;
+
+--
+-- PERIOD UQ: test inserts
+--
+
+-- okay:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-02', '2018-02-03');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-03-03', '2018-04-04');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2018-01-01', '2018-01-05');
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES (NULL, '2018-01-01', '2018-01-05');
+
+-- should fail:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2018-01-01', '2018-01-05');
+-- rejects empty:
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[3,4)', '2020-01-01', '2020-01-01');
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+
+--
+-- PERIOD UQ: test updates
+--
+
+-- update the scalar part
+UPDATE  temporal_per3
+SET     id = '[11,12)'
+WHERE   id = '[1,2)'
+AND     valid_at @> '2018-01-15'::date;
+-- update the PERIOD bounds
+UPDATE  temporal_per3
+SET     valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2018-01-15'::date;
+-- update both at once
+UPDATE  temporal_per3
+SET     id = '[21,22)',
+        valid_from = '2018-01-02',
+        valid_til = '2018-02-03'
+WHERE   id = '[11,12)'
+AND     valid_at @> '2020-01-15'::date;
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_per3
+SET     id = '[1,2)',
+        valid_from = '2018-03-05',
+        valid_til = '2018-05-05'
+WHERE   id = '[21,22)';
+-- set the scalar part to NULL (okay)
+UPDATE  temporal_per3
+SET     id = NULL,
+        valid_from = '2020-01-01',
+        valid_til = '2021-01-01'
+WHERE   id = '[21,22)';
+-- set a PERIOD bound to NULL (okay)
+UPDATE  temporal_per3
+SET     id = '[1,2)',
+        valid_from = '2018-04-04',
+        valid_til = NULL
+WHERE   id IS NULL AND valid_at @> '2020-06-01'::date;
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+-- should fail:
+UPDATE  temporal_per3
+SET     valid_from = '2018-03-01',
+        valid_til = '2018-05-05'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+-- rejects empty:
+UPDATE  temporal_per3
+SET     valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+-- still rejects empty when scalar part is NULL:
+UPDATE  temporal_per3
+SET     id = NULL,
+        valid_from = '2020-01-01',
+        valid_til = '2020-01-01'
+WHERE   id = '[1,2)' AND valid_til IS NULL;
+SELECT * FROM temporal_per3 ORDER BY id, valid_at;
+DROP TABLE temporal_per3;
+
 --
 -- test a range with both a PK and a UNIQUE constraint
 --
@@ -906,6 +1216,7 @@ CREATE TABLE temporal_fk_rng2rng (
 	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng (id, PERIOD valid_at)
 );
+\d temporal_fk_rng2rng
 DROP TABLE temporal_fk_rng2rng;
 
 -- with mismatched PERIOD columns:
@@ -2522,6 +2833,885 @@ INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VA
 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 FOREIGN KEY, PERIOD references PERIOD
+--
+
+-- test table setup
+DROP TABLE temporal_per;
+CREATE TABLE temporal_per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal_per
+	ADD CONSTRAINT temporal_per_pk
+	PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from int,
+	valid_til int,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+
+-- works: PERIOD for both referenced and referencing
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_per2per
+DROP TABLE temporal_fk_per2per;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+		REFERENCES temporal_per
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_per2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+		REFERENCES temporal_per
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+		REFERENCES temporal_per (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_per2;
+CREATE TABLE temporal_per2 (
+	id1 int4range,
+	id2 int4range,
+	valid_from date,
+	valid_til date,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_per2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2per
+DROP TABLE temporal_fk2_per2per;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2per (
+	id int4range,
+	valid_from date,
+	valid_til date,
+	parent_id1 int4range,
+	parent_id2 int4range,
+	PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2per
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+	FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+	REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2per
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	DROP PERIOD FOR valid_at,
+	ALTER COLUMN valid_from TYPE timestamp,
+	ALTER COLUMN valid_til TYPE timestamp,
+	ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+	DROP PERIOD FOR valid_at,
+	ALTER COLUMN valid_from TYPE date,
+	ALTER COLUMN valid_til TYPE date,
+	ADD PERIOD FOR valid_at (valid_from, valid_til);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk2
+	FOREIGN KEY (parent_id, PERIOD parent_id)
+	REFERENCES temporal_per (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_per2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+-- okay again:
+DELETE FROM temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2per_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+
+--
+-- test FK referencing updates
+--
+
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+UPDATE temporal_fk_per2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK parent updates NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK parent updates RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK parent deletes NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK parent deletes RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+	ADD CONSTRAINT temporal_fk_per2per_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_per
+	ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- per2per test ON UPDATE/DELETE options
+--
+-- TOC:
+-- parent updates CASCADE
+-- parent deletes CASCADE
+-- parent updates SET NULL
+-- parent deletes SET NULL
+-- parent updates SET DEFAULT
+-- parent deletes SET DEFAULT
+-- parent updates CASCADE (two scalar cols)
+-- parent deletes CASCADE (two scalar cols)
+-- parent updates SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols)
+-- parent deletes SET NULL (two scalar cols, SET NULL subset)
+-- parent updates SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols)
+-- parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+
+--
+-- test FK parent updates CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent updates SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent updates SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk_per2per_fk,
+	ADD CONSTRAINT temporal_fk_per2per_fk
+		FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_per
+		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK parent deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+	DROP CONSTRAINT temporal_fk2_per2per_fk,
+	ADD CONSTRAINT temporal_fk2_per2per_fk
+		FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+		REFERENCES temporal_per2
+		ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
 -- FK with a custom range type
 
 CREATE TYPE mydaterange AS range(subtype=date);
-- 
2.42.0

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 1 Aug 2024 10:09:03 -0700

Hello,

Here is an updated set of patches, rebased because the old patches no longer applied.

Also I have a question about foreign key RESTRICT behavior and the SQL spec.

I added some tests for a particular condition:
there are two adjacent referenced rows (sharing a scalar key part),
and a single referencing row whose time spans the transition between the referenced rows.
So graphing the records on a timeline, they look like this:

PK:  |-----|-----|
FK:     |-----|

Now suppose you simultaneously update both referenced rows to be like so:

PK:  |---------|-|
FK:     |-----|

Note that the FK's condition is still fulfilled.

In a NO ACTION constraint, we clearly should not raise an error (and we don't).

In a RESTRICT constraint, we *do* raise an error (but maybe we shouldn't).

Here is some specific SQL (added to the tests in these patches):

-- A PK update sliding the edge between two referenced rows:
INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[6,7)', daterange('2018-01-01', '2018-02-01')),
   ('[6,7)', daterange('2018-02-01', '2018-03-01'));
INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)');
UPDATE temporal_rng
SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01'
                     THEN daterange('2018-01-01', '2018-01-05')
                     WHEN lower(valid_at) = '2018-02-01'
                     THEN daterange('2018-01-05', '2018-03-01') END
WHERE id = '[6,7)';

or if you prefer PERIODs:

-- A PK update sliding the edge between two referenced rows:
INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
   ('[6,7)', '2018-01-01', '2018-02-01'),
   ('[6,7)', '2018-02-01', '2018-03-01');
INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
   ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
UPDATE temporal_per
SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
                       WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
     valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
                       WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
WHERE id = '[6,7)';

Here is what the SQL:2011 spec says (section 4.18.3.3 from Part 2 Foundation):

 > ON UPDATE RESTRICT: any change to a referenced column in the referenced table is prohibited if 
there is a matching row.

So that says we should raise an error.
But it seems clearly written with only non-temporal constraints in mind.
Is it really correct in the scenario above? The reference is still valid.
Does anyone know if the text has been updated in more recent versions of the standard?

Part of me is happy the standard says this, because not raising an error is harder to implement.
Maybe a lot harder.

On the other hand, what if we have just one row in each table, and we *expand* the referenced range? 
In other words, from this:

PK:  |-----|
FK:    |-|

to this:

PK: |-------|
FK:    |-|

Should that raise an error too? Currently it does not.

But I think that is correct. As usual I go back to Date's model about "one row per millisecond".
The referenced milliseconds didn't get updated, only the unreferenced ones.
So I think what we are doing is okay.

Likewise that same principle indicates we are doing the right thing in the original case:
we did update the referenced milliseconds.
Even though we swapped in replacements, we have to raise an error.
This is no different than the non-temporal case.

So my conclusion is we are doing the right thing in all places.
But here is an opportunity for people to disagree. :-)

Rebased to f5f30c22ed.

Yours,

From 7e7a4d8bc5bcad2326ba0fcd34f4bf23b2640002 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 19 Jan 2024 15:41:44 +0100
Subject: [PATCH v38 1/8] Add stratnum GiST support function

 

 
 
 PG_MODULE_MAGIC;

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
 
 /*
 
 # gin
index 54b50ee5d61..2eb328fec70 100644
@@ -12241,4 +12241,10 @@

From 9ad3c1a2bc891eb0eec2eb09de72094c2ca560e9 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 24 Jan 2024 15:43:41 +0100
Subject: [PATCH v38 2/8] Add temporal PRIMARY KEY and UNIQUE constraints

 src/backend/commands/indexcmds.c              |  143 +-
 .../regress/expected/without_overlaps.out     | 1264 +++++++++++++++++
 src/test/regress/sql/without_overlaps.sql     |  867 +++++++++++
 33 files changed, 2582 insertions(+), 52 deletions(-)

 
 
+     <row>
 
  <variablelist>
 
+      <para>
index 93b3f664f21..b791aee8357 100644

 
     <listitem>
 
+     <para>
 
      <para>
 
      <para>

      <para>
 
 	PG_RETURN_UINT16(strat);
 }
+
+/*
 
 	pfree(ccbin);

 
 	/*
 
 	if (conkeyArray)

 
 
+	/*

 	/*
 
-						if (stmt->unique)

 	indexRelationId =

 
 		/*
 
+/*
index 0b2a52463f1..53222974c83 100644
@@ -10159,6 +10159,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10457,6 +10458,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conPeriod */
 									  false);
 
 			/*
@@ -10962,6 +10964,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conPeriod */
 								  true);
 
@@ -14131,7 +14134,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)

 
 	if (constrAddr)

+		/*
 
+opt_without_overlaps:

 
+		/*
 
 	/*

 

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 0d025162738..e74b7bfbf0f 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7366,6 +7366,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conperiod,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7447,10 +7448,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7518,6 +7526,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7625,6 +7634,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -17056,6 +17066,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conperiod)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
index 5bcc2244d58..ab6c8304913 100644

 
 #include "access/itup.h"

 
+	/*
 
 	/*
 
 	/*

 
 
index 00000000000..19586c47b55
@@ -0,0 +1,1264 @@
+LINE 3:   CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHO...
+LINE 3:   CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT O...
+-- ON CONFLICT: ranges
+-- ON CONFLICT: multiranges
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+ [1,2) | {[2010-01-01,2020-01-01)}
+ [2,3) | {[2005-01-01,2006-01-01)}
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+ [1,2) | {[2010-01-01,2020-01-01)}
+ [2,3) | {[2005-01-01,2006-01-01)}
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ [1,2) | {[2000-01-01,2010-01-01)}
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ [1,2) | {[2000-01-01,2010-01-01)}
+  CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+ [1,2) | {[2010-01-01,2020-01-01)}
+ [2,3) | {[2005-01-01,2006-01-01)}
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+ [1,2) | {[2000-01-01,2010-01-01)}
+ [1,2) | {[2010-01-01,2020-01-01)}
+ [2,3) | {[2005-01-01,2006-01-01)}
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ [1,2) | {[2000-01-01,2010-01-01)}
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ [1,2) | {[2000-01-01,2010-01-01)}
 
index 00000000000..cc7126b8531
@@ -0,0 +1,867 @@
+-- ON CONFLICT: ranges
+-- ON CONFLICT: multiranges
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO NOTHING;
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+TRUNCATE temporal_mltrng;
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+  CONSTRAINT temporal_mltrng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT DO NOTHING;
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO NOTHING;
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO NOTHING;
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+TRUNCATE temporal_mltrng3;
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2000-01-01', '2010-01-01')));
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[1,2)', datemultirange(daterange('2010-01-01', '2020-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+INSERT INTO temporal_mltrng3 (id, valid_at) VALUES ('[2,3)', datemultirange(daterange('2005-01-01', '2006-01-01'))) ON CONFLICT ON CONSTRAINT temporal_mltrng3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';

From 22a5d20df51bb368dc4981289d5cdd1ed0ea5545 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 1 Jun 2024 15:24:56 -0700
Subject: [PATCH v38 3/8] Forbid empty ranges/multiranges in WITHOUT OVERLAPS
 columns

 
       <para>
index b791aee8357..1d7939a12d8 100644
 
      <para>
 
 	/*

 
-
 /*

 
+	/*

 
 
 							/*

 /*
 
index 19586c47b55..96aaa373241 100644
+LINE 4:   CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHO...
+LINE 4:   CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT O...
index cc7126b8531..e05fa1d00c0 100644

 

 -- should fail:

 

 
 
 -- should fail:

-- 
2.42.0

From f6c0bffa974e002c4be5fa730c1109dfc13b8249 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 24 Mar 2024 07:37:13 +0100
Subject: [PATCH v38 4/8] Add temporal FOREIGN KEY contraints

Reference actions ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}
are not supported yet.
---
 .../btree_gist/expected/without_overlaps.out  |   48 +
 contrib/btree_gist/sql/without_overlaps.sql   |   28 +
 doc/src/sgml/catalogs.sgml                    |    3 +-
 doc/src/sgml/ref/create_table.sgml            |   45 +-
 src/backend/catalog/pg_constraint.c           |   58 +
 src/backend/commands/indexcmds.c              |   31 +-
 src/backend/commands/tablecmds.c              |  226 ++-
 src/backend/parser/gram.y                     |   45 +-
 src/backend/utils/adt/ri_triggers.c           |  169 ++-
 src/backend/utils/adt/ruleutils.c             |   17 +-
 src/include/catalog/pg_constraint.h           |   21 +-
 src/include/commands/defrem.h                 |    2 +-
 src/include/nodes/parsenodes.h                |    2 +
 src/include/parser/kwlist.h                   |    1 +
 .../regress/expected/without_overlaps.out     | 1283 +++++++++++++++++
 src/test/regress/sql/without_overlaps.sql     | 1234 ++++++++++++++++
 16 files changed, 3105 insertions(+), 108 deletions(-)

index 1d7939a12d8..bc2bfacb6d7 100644

 

+/*
+ * FindFkPeriodOpers -
+ *

 	*opid = InvalidOid;

 			ereport(ERROR,

 	if (!OidIsValid(*opid))
 
 		ereport(ERROR,
index 53222974c83..82404ab7aa3 100644

 
 				/*
@@ -9593,6 +9600,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9687,6 +9696,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9706,18 +9720,40 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
+	/*
@@ -9751,6 +9787,28 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
+	/*
@@ -9797,16 +9855,56 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
-		/*
 
 		/*
 		 * There had better be a primary equality operator for the index.
@@ -9956,6 +10054,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		ffeqoperators[i] = ffeqop;
 	}
 
+	/*
@@ -9972,7 +10086,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9988,7 +10103,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10073,7 +10189,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10159,7 +10276,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10235,7 +10352,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10293,7 +10411,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10341,6 +10460,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10458,7 +10578,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conPeriod */
+									  with_period,	/* conPeriod */
 									  false);
 
 			/*
@@ -10489,7 +10609,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									with_period);
 
 			table_close(partition, NoLock);
 		}
@@ -10725,7 +10846,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conperiod);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10818,6 +10940,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		with_period;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10933,6 +11056,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
@@ -10964,7 +11088,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
 		/* Set up partition dependencies for the new constraint */
@@ -10998,7 +11122,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								with_period);
 		table_close(pkrel, NoLock);
 	}
 
@@ -11809,7 +11934,8 @@ transformColumnNameList(Oid relId, List *colList,
  *
  *	Look up the names, attnums, and types of the primary key attributes
  *	for the pkrel.  Also return the index OID and index opclasses of the
- *	index supporting the primary key.
+ *	index supporting the primary key.  Also return whether the index has
+ *	WITHOUT OVERLAPS.
  *
  *	All parameters except pkrel are output parameters.  Also, the function
  *	return value is the number of attributes in the primary key.
@@ -11820,7 +11946,7 @@ static int
 transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 						   List **attnamelist,
 						   int16 *attnums, Oid *atttypids,
-						   Oid *opclasses)
+						   Oid *opclasses, bool *pk_has_without_overlaps)
 {
 	List	   *indexoidlist;
 	ListCell   *indexoidscan;
@@ -11898,6 +12024,8 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 							   makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
 	}

 	return i;
@@ -11911,14 +12039,16 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
  *
@@ -11965,12 +12095,12 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 		/*
@@ -12008,6 +12138,13 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12023,6 +12160,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 				found_deferrable = true;
 				found = false;
 			}
+
+			/* We need to know whether the index has WITHOUT OVERLAPS */
+			if (found)
+				*pk_has_without_overlaps = indexStruct->indisexclusion;
 		}
 		ReleaseSysCache(indexTuple);
 		if (found)
@@ -12117,7 +12258,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool hasperiod)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12145,9 +12287,11 @@ validateForeignKeyConstraint(char *conname,
 
 	/*
 
 	/*
@@ -12298,6 +12442,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12358,6 +12503,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

 

+optionalPeriodName:
index 62601a6d80c..db30a6cc1e4 100644

 
@@ -494,14 +541,40 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
+		 * the old PK's range is completely covered.
@@ -518,6 +591,18 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 
@@ -2162,6 +2247,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2173,6 +2259,20 @@ ri_LoadConstraintInfo(Oid constraintOid)
 							   &riinfo->ndelsetcols,
 							   riinfo->confdelsetcols);
 
+	/*
 
 	/*
@@ -2784,7 +2884,10 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
 /*
  * ri_KeysEqual -
  *
@@ -2840,13 +2943,25 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
@@ -2856,29 +2971,31 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,

 /*
- * ri_AttributesEqual -
+ * ri_CompareWithCast -
  *

 	/*
@@ -2892,10 +3009,16 @@ ri_AttributesEqual(Oid eq_opr, Oid typeid,
 
 /*
@@ -2950,7 +3073,7 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid)

 

 

 
 	return nKeys;
 
 	/*

 	/*
 
 	/*
 
 	/*

 
index 96aaa373241..0608d250784 100644
@@ -1333,4 +1333,1287 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 (1 row)
 
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-02-20') WHERE id = '[1,2)';
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-02-20)) is not present in table "temporal_rng".
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', daterange('2018-01-01', '2018-02-01')),
+  ('[6,7)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', daterange('2018-01-01', '2018-02-01')),
+  ('[6,7)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+DETAIL:  Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-02-20')) WHERE id = '[1,2)';
+DETAIL:  Key (parent_id, valid_at)=([8,9), {[2018-01-02,2018-02-20)}) is not present in table "temporal_mltrng".
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
+                    WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
+                    WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
+DETAIL:  Key (id, valid_at)=([6,7), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- FK between partitioned tables: ranges
+-- FK between partitioned tables: multiranges
+CREATE TABLE temporal_partitioned_mltrng (
+  CONSTRAINT temporal_paritioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_mltrng 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_mltrng 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_mltrng (id, valid_at, name) VALUES
+  ('[1,2)', datemultirange(daterange('2000-01-01', '2000-02-01')), 'one'),
+  ('[1,2)', datemultirange(daterange('2000-02-01', '2000-03-01')), 'one'),
+  ('[2,3)', datemultirange(daterange('2000-01-01', '2010-01-01')), 'two');
+CREATE TABLE temporal_partitioned_fk_mltrng2mltrng (
+  CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_partitioned_mltrng (id, PERIOD valid_at)
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_mltrng2mltrng 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_mltrng2mltrng 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_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[1,2)'),
+  ('[1,2)', datemultirange(daterange('2001-01-01', '2002-01-01')), '[2,3)'),
+  ('[2,3)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[1,2)');
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2010-01-01', '2010-02-15')), '[1,2)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), {[2010-01-01,2010-02-15)}) is not present in table "temporal_partitioned_mltrng".
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[3,4)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_fk"
+DETAIL:  Key (parent_id, valid_at)=([3,4), {[2000-01-01,2000-02-15)}) is not present in table "temporal_partitioned_mltrng".
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2000-01-01', '2000-02-13')) WHERE id = '[2,3)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET id = '[4,5)' WHERE id = '[1,2)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET id = '[1,2)' WHERE id = '[4,5)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2000-01-01', '2000-04-01')) WHERE id = '[1,2)';
+DETAIL:  Key (id, valid_at)=([1,2), {[2000-01-01,2000-04-01)}) conflicts with existing key (id, valid_at)=([1,2), {[2000-01-01,2000-04-01)}).
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2016-01-01', '2016-02-01')));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')) WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_parent_id_valid_at_fkey1" on table "temporal_partitioned_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_partitioned_fk_mltrng2mltrng".
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_parent_id_valid_at_fkey1" on table "temporal_partitioned_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_partitioned_fk_mltrng2mltrng".
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+  REFERENCES temporal_partitioned_mltrng
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2016-01-01', '2016-02-01')));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')) WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_parent_id_valid_at_fkey1" on table "temporal_partitioned_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_partitioned_fk_mltrng2mltrng".
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_mltrng2mltrng_parent_id_valid_at_fkey1" on table "temporal_partitioned_fk_mltrng2mltrng"
+DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_partitioned_fk_mltrng2mltrng".
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
+DROP TABLE temporal_partitioned_mltrng;
index e05fa1d00c0..943edf3da63 100644
@@ -920,4 +920,1238 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;

+--
+-- test FK dependencies
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_rng2rng SET valid_at = daterange('2018-01-02', '2018-02-20') WHERE id = '[1,2)';
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', daterange('2018-01-01', '2018-02-01')),
+  ('[6,7)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', daterange('2018-01-01', '2018-02-01')),
+  ('[6,7)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-02-20')) WHERE id = '[1,2)';
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
+                    WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)');
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
+                    WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
+-- FK between partitioned tables: ranges
+-- FK between partitioned tables: multiranges
+CREATE TABLE temporal_partitioned_mltrng (
+  CONSTRAINT temporal_paritioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_mltrng 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_mltrng 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_mltrng (id, valid_at, name) VALUES
+  ('[1,2)', datemultirange(daterange('2000-01-01', '2000-02-01')), 'one'),
+  ('[1,2)', datemultirange(daterange('2000-02-01', '2000-03-01')), 'one'),
+  ('[2,3)', datemultirange(daterange('2000-01-01', '2010-01-01')), 'two');
+CREATE TABLE temporal_partitioned_fk_mltrng2mltrng (
+  CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_partitioned_mltrng (id, PERIOD valid_at)
+CREATE TABLE tfkp1 PARTITION OF temporal_partitioned_fk_mltrng2mltrng 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_mltrng2mltrng 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_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[1,2)'),
+  ('[1,2)', datemultirange(daterange('2001-01-01', '2002-01-01')), '[2,3)'),
+  ('[2,3)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[1,2)');
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2010-01-01', '2010-02-15')), '[1,2)');
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2000-01-01', '2000-02-15')), '[3,4)');
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2000-01-01', '2000-02-13')) WHERE id = '[2,3)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET id = '[4,5)' WHERE id = '[1,2)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET id = '[1,2)' WHERE id = '[4,5)';
+UPDATE temporal_partitioned_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2000-01-01', '2000-04-01')) WHERE id = '[1,2)';
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2016-01-01', '2016-02-01')));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')) WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk;
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+  REFERENCES temporal_partitioned_mltrng
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2016-01-01', '2016-02-01')));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')) WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+  WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+UPDATE temporal_partitioned_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+TRUNCATE temporal_partitioned_mltrng, temporal_partitioned_fk_mltrng2mltrng;
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01')));
+INSERT INTO temporal_partitioned_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_partitioned_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
+DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
+  DROP CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk,
+  ADD CONSTRAINT temporal_partitioned_fk_mltrng2mltrng_fk
+    REFERENCES temporal_partitioned_mltrng
+DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
+DROP TABLE temporal_partitioned_mltrng;
+
 RESET datestyle;
-- 
2.42.0

From 97f25db5f7e359bbcf08c1754e435480fdacf1c4 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v38 5/8] Add support funcs for FOR PORTION OF

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>

 	va_start(ap, maxargs);
 
 /*
 
+/*

 
 Datum

 /*
 
 # gin
index 2eb328fec70..135ea0b4ab2 100644
@@ -10637,6 +10637,10 @@
@@ -10924,6 +10928,10 @@

+-- without_portion

+-- without_portion

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

index 212434711e3..2c1c58961e1 100644
 
index 371e1319338..684c004adb9 100644

 
index 7717855bc9e..eade59b93ab 100644
     [ RETURNING { * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
 
+  <para>
 
+   <varlistentry>
index 1c433bec2bb..cfa158ed95b 100644
 
+  <para>
 
+   <varlistentry>

 

index 82404ab7aa3..b91a7455b11 100644
@@ -12331,6 +12331,7 @@ validateForeignKeyConstraint(char *conname,

 

 

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

 /*

 

+	/*

 
index 54e042a8a59..f9a12716769 100644
@@ -3725,7 +3725,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3791,6 +3791,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
 #include "postgres.h"

@@ -58,10 +63,16 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*

 

 					n->relation = $4;
-					n->usingClause = $5;
-					n->whereClause = $6;
-					n->returningList = $7;
+					n->forPortionOf = $5;
+					n->usingClause = $6;
+					n->whereClause = $7;
+					n->returningList = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}

 					n->relation = $3;
-					n->targetList = $5;
-					n->fromClause = $6;
-					n->whereClause = $7;
-					n->returningList = $8;
+					n->forPortionOf = $4;
+					n->targetList = $6;
+					n->fromClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
 
+for_portion_of_clause:
 
 			/*
 
 			/*
index d2db69a3f90..5c99bb8848f 100644
 
 			/*
 
 			/*
 
 			/*
 
index db30a6cc1e4..cbfb50f1287 100644

 /*
 
@@ -615,6 +622,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,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -714,6 +722,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
 
@@ -804,9 +814,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -913,6 +930,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1034,6 +1052,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1266,6 +1285,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,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2413,6 +2433,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2468,6 +2489,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3144,3 +3171,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *

+/*
+ * get_typname
+ *

 #endif							/* STRATNUM_H */
 
 /*

+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

 
+/*----------
index f00bd55f393..0002dcf782a 100644
index 00000000000..11ab59aeb40
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 0608d250784..ab96ea9bf10 100644

@@ -1707,6 +1797,18 @@ UPDATE temporal_rng SET id = '[7,8)'
@@ -1760,6 +1862,18 @@ UPDATE temporal_rng SET id = '[7,8)'
@@ -1796,9 +1910,22 @@ BEGIN;
@@ -1827,9 +1954,22 @@ BEGIN;
 
 # ----------
index 00000000000..96ad87c9cf1

 
index 943edf3da63..bab34659793 100644

 -- temporal UNIQUE:
 
 -- ALTER TABLE REPLICA IDENTITY
@@ -1262,6 +1308,16 @@ 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)';
 -- 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')
@@ -1311,6 +1367,16 @@ ROLLBACK;
 -- 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)';
 -- 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')
@@ -1346,9 +1412,20 @@ BEGIN;

@@ -1377,9 +1454,20 @@ BEGIN;

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

---
 doc/src/sgml/ref/create_table.sgml            |   14 +-
 src/backend/catalog/pg_constraint.c           |   20 +-
 src/backend/commands/tablecmds.c              |   83 +-
 src/backend/utils/adt/ri_triggers.c           |  584 +++++-
 src/include/catalog/pg_constraint.h           |    7 +-
 src/include/catalog/pg_proc.dat               |   22 +
 src/test/regress/expected/btree_index.out     |   18 +-
 .../regress/expected/without_overlaps.out     | 1732 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     |  977 +++++++++-
 9 files changed, 3340 insertions(+), 117 deletions(-)

index bc2bfacb6d7..8cae17d3f46 100644
 
          <para>
 
          <para>
 
          <para>
 
 /*
- * FindFkPeriodOpers -
 
 /*
index b91a7455b11..90c6c8e46e8 100644
@@ -9602,6 +9602,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9697,15 +9698,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -9787,28 +9792,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
-	/*
@@ -10066,8 +10049,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
+		Oid			intersectprocoid;

 	/*
@@ -10121,6 +10108,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10131,6 +10119,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
@@ -12459,17 +12454,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12520,17 +12524,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index cbfb50f1287..f05a65e8ee5 100644

 
@@ -817,7 +825,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
 	ri_PerformCheck(riinfo, &qkey, qplan,
@@ -1316,6 +1324,535 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
@@ -2288,9 +2825,10 @@ ri_LoadConstraintInfo(Oid constraintOid)

 	ReleaseSysCache(tup);
@@ -2445,8 +2983,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
 
 	/*
@@ -2481,8 +3019,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -3162,6 +3702,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 
@@ -3190,30 +3736,30 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn

 
index 135ea0b4ab2..3fb8c49f9f2 100644
@@ -3993,6 +3993,28 @@

 
 explain (costs off)

 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 ab96ea9bf10..55264298b5e 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
@@ -1809,6 +1809,21 @@ 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')
@@ -1874,6 +1889,21 @@ 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')
@@ -1920,12 +1950,22 @@ 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
 -- test FK referenced deletes RESTRICT
 --
@@ -1964,39 +2004,206 @@ 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 --
--- test ON UPDATE/DELETE options
+-- 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 ('[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
   DROP CONSTRAINT temporal_fk_rng2rng_fk,
   ADD CONSTRAINT temporal_fk_rng2rng_fk
     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
-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
   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 ('[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]',
   DROP CONSTRAINT temporal_fk_rng2rng_fk,
@@ -2004,7 +2211,432 @@ ALTER TABLE temporal_fk_rng2rng
     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
 --
@@ -2335,6 +2967,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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 --
 -- test FK referenced updates RESTRICT
 --
@@ -2384,6 +3032,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 valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 --
 -- test FK referenced deletes NO ACTION
 --
@@ -2416,6 +3080,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'));
 --
 -- test FK referenced deletes RESTRICT
 --
@@ -2444,6 +3121,638 @@ BEGIN;
 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".
 ROLLBACK;
+-- 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'));
+--
+-- 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
 --
@@ -2453,8 +3762,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
   CONSTRAINT temporal_paritioned_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'),
@@ -2467,8 +3776,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
 --
@@ -2510,7 +3819,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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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
@@ -2522,7 +3831,7 @@ 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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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 RESTRICT
@@ -2544,7 +3853,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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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 RESTRICT
@@ -2556,37 +3865,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_parent_id_valid_at_fkey" on table "temporal_partitioned_fk_rng2rng"
+ERROR:  update or delete on table "tp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_parent_id_valid_at_fkey1" 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,
@@ -2594,10 +4028,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;
 --
@@ -2717,32 +4214,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,
@@ -2750,10 +4365,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 bab34659793..a8f41020e0a 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.

@@ -1318,6 +1318,8 @@ 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')
@@ -1377,6 +1379,8 @@ 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')
@@ -1420,12 +1424,11 @@ WHERE id = '[5,6)';
 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 
@@ -1462,41 +1465,131 @@ WHERE id = '[5,6)';

+--
 
+--
 
@@ -1504,6 +1597,252 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -1807,6 +2146,20 @@ COMMIT;
 
@@ -1853,6 +2206,20 @@ ROLLBACK;
 
 --
 -- test FK referenced deletes NO ACTION
@@ -1884,6 +2251,17 @@ BEGIN;

 --
 -- test FK referenced deletes RESTRICT
@@ -1913,6 +2291,417 @@ BEGIN;

 --
 -- FK between partitioned tables: ranges
@@ -1924,8 +2713,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -1939,8 +2728,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
 
 --
 -- partitioned FK referencing inserts
@@ -2033,36 +2822,90 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2070,11 +2913,34 @@ ALTER TABLE temporal_partitioned_fk_rng2rng

 
@@ -2197,36 +3063,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)';

+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)';

+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,
@@ -2234,11 +3154,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)';

+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.42.0

From 2569501697e103bfbb943757eff60856adb20a25 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v38 8/8] Add PERIODs

 doc/src/sgml/catalogs.sgml                    |  112 +
 src/backend/commands/tablecmds.c              |  866 ++++++-
 .../regress/expected/without_overlaps.out     | 2031 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 1449 +++++++++++-
 59 files changed, 6296 insertions(+), 145 deletions(-)

+     <row>

 

index 8cae17d3f46..87456920a52 100644

 
+  <para>

 
      <para>
index eade59b93ab..698ee76a333 100644
 
   <para>
index cfa158ed95b..163fe1c5604 100644
 
   <para>
 
+					/*
 
+		case PeriodRelationId:
 
+/*

 /*
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case PeriodRelationId:
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 90c6c8e46e8..d9d39181521 100644

+	/*
 
+	/*
 
 	/*
 
+/*

+/*----------
+ * MergePeriods

 
 /*
 
@@ -7415,14 +8012,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7466,6 +8078,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -7996,6 +8679,157 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -9770,8 +10604,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -13555,6 +14390,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 
 				/*
@@ -13644,6 +14489,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -15537,7 +16391,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*

 		/*
 
+	/*

 

+
 TypedTableElement:

+TablePeriod:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

 
+			case AT_AddPeriod:
+				{
+					/*

 /*
 
+Oid
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 8c20c263c4b..bf2f55d5a15 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3704,6 +3704,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 			 strcmp(type, "DATABASE PROPERTIES") == 0 ||
 			 strcmp(type, "DEFAULT") == 0 ||
 			 strcmp(type, "FK CONSTRAINT") == 0 ||
+			 strcmp(type, "PERIOD") == 0 ||
 			 strcmp(type, "INDEX") == 0 ||
 			 strcmp(type, "RULE") == 0 ||
 			 strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e74b7bfbf0f..4ec47b990aa 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6733,6 +6733,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reltype;
 	int			i_relowner;
 	int			i_relchecks;
+	int			i_nperiod;
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
@@ -6810,6 +6811,14 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
@@ -6947,6 +6956,7 @@ getTables(Archive *fout, int *numTables)
@@ -7030,6 +7040,7 @@ getTables(Archive *fout, int *numTables)
@@ -7453,7 +7464,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -8663,7 +8674,7 @@ getTransforms(Archive *fout)
@@ -8713,6 +8724,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8727,7 +8740,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9089,15 +9103,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9119,6 +9154,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9138,12 +9174,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9202,6 +9239,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10430,6 +10541,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15983,6 +16096,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -15991,7 +16131,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16237,7 +16377,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 */
 			resetPQExpBuffer(extra);
 			firstitem = true;
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -16543,7 +16683,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18615,6 +18755,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:

 

+		/* print periods */

 
 
 /*

 

 #endif							/* TABLECMDS_H */

+
+/* ----------
 
index 11ab59aeb40..38bfcd1777d 100644

 \d gtest1
                             Table "public.gtest1"
index 55264298b5e..c61949f8200 100644

 
@@ -1424,32 +1944,210 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
 DROP TABLE temporal_mltrng3;
 --
--- test FK dependencies
+-- ON CONFLICT: PERIODs
 --
--- can't drop a range referenced by an FK, unless with CASCADE
-CREATE TABLE temporal3 (
-  id int4range,
-  valid_at daterange,
-  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
-);
-CREATE TABLE temporal_fk_rng2rng (
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+ [1,2) | 2010-01-01 | 2020-01-01
+ [2,3) | 2005-01-01 | 2006-01-01
+(3 rows)
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+ [1,2) | 2010-01-01 | 2020-01-01
+ [2,3) | 2005-01-01 | 2006-01-01
+(3 rows)
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+-- with a UNIQUE constraint:
+CREATE TABLE temporal_per3 (
   id int4range,
-  valid_at daterange,
-  parent_id int4range,
-  CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
-  CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
-    REFERENCES temporal3 (id, PERIOD valid_at)
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
 );
-ALTER TABLE temporal3 DROP COLUMN valid_at;
-ERROR:  cannot drop column valid_at of table temporal3 because other objects depend on it
-DETAIL:  constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
-HINT:  Use DROP ... CASCADE to drop the dependent objects too.
-ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
-NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
-DROP TABLE temporal_fk_rng2rng;
-DROP TABLE temporal3;
---
--- test FOREIGN KEY, range references range
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+ [1,2) | 2010-01-01 | 2020-01-01
+ [2,3) | 2005-01-01 | 2006-01-01
+(3 rows)
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+ [1,2) | 2010-01-01 | 2020-01-01
+ [2,3) | 2005-01-01 | 2006-01-01
+(3 rows)
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [1,2) | 2000-01-01 | 2010-01-01
+(1 row)
+
+DROP TABLE temporal_per3;
+--
+-- test FK dependencies
+--
+-- can't drop a range referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_at daterange,
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_rng2rng (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP COLUMN valid_at;
+ERROR:  cannot drop column valid_at of table temporal3 because other objects depend on it
+DETAIL:  constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng depends on column valid_at of table temporal3
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+DROP TABLE temporal_fk_rng2rng;
+DROP TABLE temporal3;
+--
+-- test FOREIGN KEY, range references range
 --
 -- test table setup
 DROP TABLE temporal_rng;
@@ -1477,6 +2175,18 @@ CREATE TABLE temporal_fk_rng2rng (
   CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng (id, PERIOD valid_at)
 );
+\d temporal_fk_rng2rng
+           Table "public.temporal_fk_rng2rng"
+  Column   |   Type    | Collation | Nullable | Default 
+-----------+-----------+-----------+----------+---------
+ id        | int4range |           | not null | 
+ valid_at  | daterange |           | not null | 
+ parent_id | int4range |           |          | 
+Indexes:
+    "temporal_fk_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk_rng2rng_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+
 DROP TABLE temporal_fk_rng2rng;
 -- with mismatched PERIOD columns:
 -- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
@@ -3725,6 +4435,1267 @@ SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, val
  [200,201) | {[2020-01-01,2021-01-01)} | [8,9)      | [8,9)
 (2 rows)
 
+--
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-02-20)) is not present in table "temporal_per".
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+DETAIL:  Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
@@ -4428,4 +6399,18 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
+--
+-- FK between partitioned tables: PERIODs
+--
+CREATE TABLE temporal_partitioned_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  name text,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_per 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)');
+ERROR:  Inheriting from a table with a PERIOD is not supported
+DROP TABLE temporal_partitioned_per;
 RESET datestyle;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5e9693a1d24..d9225cf3685 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -43,7 +43,7 @@ test: copy copyselect copydml insert insert_conflict
 # Note: many of the tests in later groups depend on create_index
 # ----------
 test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema
-test: create_index create_index_spgist create_view index_including index_including_gist
+test: create_index create_index_spgist create_view index_including index_including_gist periods
 
 # ----------
index 96ad87c9cf1..17845e9e2f7 100644

+-- Test with PERIODs
+
 
index a8f41020e0a..2ddbd84c36f 100644

 
 
+--
@@ -966,6 +1276,121 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO NOTHING;
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per_pk DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_per ORDER BY id, valid_from, valid_til;
+
+-- with a UNIQUE constraint:
+
+CREATE TABLE temporal_per3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT DO NOTHING;
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO NOTHING;
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO NOTHING;
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT (id, valid_at) DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+
+TRUNCATE temporal_per3;
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2000-01-01', '2010-01-01');
+-- with a conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[2,3)';
+-- id matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[1,2)', '2010-01-01', '2020-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[3,4)';
+-- date matches but no conflict
+INSERT INTO temporal_per3 (id, valid_from, valid_til) VALUES ('[2,3)', '2005-01-01', '2006-01-01') ON CONFLICT ON CONSTRAINT temporal_per3_uq DO UPDATE SET id = EXCLUDED.id + '[4,5)';
+SELECT * FROM temporal_per3 ORDER BY id, valid_from, valid_til;
+
+DROP TABLE temporal_per3;
+
 --
 -- test FK dependencies
 --
@@ -1019,6 +1444,7 @@ CREATE TABLE temporal_fk_rng2rng (
   CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng (id, PERIOD valid_at)
 );
+\d temporal_fk_rng2rng
 DROP TABLE temporal_fk_rng2rng;
 
 -- with mismatched PERIOD columns:
@@ -2677,70 +3103,974 @@ INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VA
 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);
+--
+-- test FOREIGN KEY, PERIOD references PERIOD
+--
 
-CREATE TABLE temporal_rng3 (
+-- test table setup
+DROP TABLE temporal_per;
+CREATE TABLE temporal_per (
   id int4range,
-  valid_at mydaterange,
-  CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
 );
-CREATE TABLE temporal_fk3_rng2rng (
+ALTER TABLE temporal_per
+  ADD CONSTRAINT temporal_per_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2per (
   id int4range,
-  valid_at mydaterange,
+  valid_from int,
+  valid_til int,
   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
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
 );
-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;
+-- works: PERIOD for both referenced and referencing
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_per2per
+DROP TABLE temporal_fk_per2per;
 
---
--- FK between partitioned tables: ranges
---
+-- with mismatched PERIOD columns:
 
-CREATE TABLE temporal_partitioned_rng (
-  valid_at daterange,
-  name text,
-  CONSTRAINT temporal_paritioned_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)', '[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'),
-  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
-CREATE TABLE temporal_partitioned_fk_rng2rng (
-  valid_at daterange,
   parent_id int4range,
-  CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
-  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)', '[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
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
-  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
-  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
-  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
--- should fail:
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+-- A PK update sliding the edge between two referenced rows:
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+-- FK between partitioned tables: ranges
   ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
 INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
@@ -3185,4 +4515,19 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
+--
+-- FK between partitioned tables: PERIODs
+--
+
+CREATE TABLE temporal_partitioned_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  name text,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+) PARTITION BY LIST (id);
+CREATE TABLE tp1 PARTITION OF temporal_partitioned_per 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)');
+DROP TABLE temporal_partitioned_per;
+
 RESET datestyle;
-- 
2.42.0

---

From: jian he <[email-masked]>
Date: Tue, 6 Aug 2024 10:02:13 +0800

> Here is an updated set of patches, rebased because the old patches no longer applied.

void
ExecWithoutOverlapsNotEmpty(Relation rel, Datum attval, Oid typtype,
Oid atttypid);

should this just be a static function?
I am not so sure.

Oid typtype
should be
char typtype
?

                 errmsg("new row for relation \"%s\" contains empty
WITHOUT OVERLAPS value",
we already have Form_pg_attribute via "TupleDesc tupdesc =
RelationGetDescr(heap);"
we can make the error message be:
                 errmsg("cannot be empty range value for WITHOUT
OVERLAPS column \"%s\" in relation \"%s\", colname,
RelationGetRelationName(rel))

elog(ERROR, "Got unknown type for WITHOUT OVERLAPS column: %d", atttypid);
people will wonder if domain over range works or not. but currently
not, better error message would be:
            elog(ERROR, "WITHOUT OVERLAPS column \"%s\" is not a range
or multirange type ", colname);
This part is unlikely to be reachable, so I don't have a strong opinion on it.

+ if (!found)
+ column = NULL;
this part no need?
because if not found, the column would be last element in ColumnDef
type list columns
also the following change also make sense:

I am confused with this change?
you found out the typid,but didn't using this information, should it be
+ if (strcmp(attname, key) == 0)
+ {
+ typid = attr->atttypid;
+ found = true;
+ break;
+ }

so the failing error message be same for the following two cases:
CREATE TABLE t1 (id int4range,valid_at tsrange,b text,
   CONSTRAINT temporal_rng_pk PRIMARY KEY (id, b WITHOUT OVERLAPS)
);

CREATE TABLE t1 (id int4range,valid_at tsrange,b text);
alter table t1 add CONSTRAINT temporal_rng_pk PRIMARY KEY (id, b
WITHOUT OVERLAPS);

---

From: jian he <[email-masked]>
Date: Tue, 6 Aug 2024 22:50:00 +0800

> > Here is an updated set of patches, rebased because the old patches no longer applied.

hi. some minor issues.

in generateClonedIndexStmt
index->iswithoutoverlaps = (idxrec->indisprimary ||
idxrec->indisunique) && idxrec->indisexclusion;
this case, the index accessMethod will be "gist" only?

do you think it's necessary to:
index->iswithoutoverlaps = (idxrec->indisprimary ||
idxrec->indisunique) && idxrec->indisexclusion
&& strcmp(index->accessMethod, "gist") == 0);

src/bin/pg_dump/pg_dump.c and src/bin/psql/describe.c
should be "if (pset.sversion >= 180000)"?

+ (This is sometimes called a
+      temporal key, if the column is a range of dates or timestamps, but
+      PostgreSQL allows ranges over any base type.)

PostgreSQL should be decorated as
<productname>PostgreSQL</productname>
?

in DefineIndex we have:
if (stmt->unique && !stmt->iswithoutoverlaps && !amRoutine->amcanunique)
if (stmt->indexIncludingParams != NIL && !amRoutine->amcaninclude)
if (numberOfKeyAttributes > 1 && !amRoutine->amcanmulticol)
if (exclusion && amRoutine->amgettuple == NULL)

maybe we can add:
    if (stmt->iswithoutoverlaps && strcmp(accessMethodName, "gist") != 0)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("access method \"%s\" does not support WITHOUT
OVERLAPS constraints",
                        accessMethodName)));

+ /* exclusionOpNames can be non-NIL if we are creating a partition */
+ if (iswithoutoverlaps && exclusionOpNames == NIL)
+ {
+ indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
+ indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols);
+ }
the comment is not 100% correct, i think.
creating a partition, "create table like INCLUDING ALL", both will go
through generateClonedIndexStmt.
generateClonedIndexStmt will produce exclusionOpNames if this index
supports exclusion constraint.

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 7 Aug 2024 13:54:52 -0700

Here are some fixes based on outstanding feedback (some old some new). Details below:

On 3/25/24 17:00, jian he wrote:
 > hi.
 > minor issues I found in v33-0003.
 > there are 29 of {check_amproc_signature?.*false}
 > only one {check_amproc_signature(procform->amproc, opcintype, true}
 > is this refactoring really worth it?

I could add a separate function, for example check_amproc_retset_signature, but it would require 
duplicating almost the whole existing function, so a param seems better here.

 > We also need to refactor gistadjustmembers?

You're right, added the new support procs there.

 > needs to add "(optional)".

Added.

 >                                                 elog(ERROR, "range
 > types do not match");
 > the elog, ERROR indentation is wrong?

Fixed.

 > the comments need to be refactored?
 > there is nothing related to "slot"?
 > not sure the "array" description is right.
 > (my understanding is compute rangetype r1 and r2, and save the result to
 > RangeType **outputs.

Changed "slots" to "elements". Everything else looks correct to me.

 > select proisstrict, proname from pg_proc where proname =
 > 'range_without_portion';
 > range_without_portion is strict.
 > but
 > select range_without_portion(NULL::int4range, int4range(11, 20,'[]'));
 > return zero rows.
 > Is this the expected behavior?

Returning zero rows is correct if the function is never called (which is what strict does).
I see other strict retset functions, e.g. json_array_elements.
That also returns zero rows if you say SELECT json_array_elements(NULL);

On 4/14/24 17:00, jian he wrote:
 > for unique index, primary key:
 > ii_ExclusionOps, ii_UniqueOps is enough to distinguish this index
 > support without overlaps,
 > we don't need another ii_HasWithoutOverlaps?
 > (i didn't test it though)

I think it is worth having something named. But also ii_Exclusion is not set in 
index_concurrently_create_copy, so inferring when we have WITHOUT OVERLAPS will not work in that case.

 > ON CONFLICT DO NOTHING
 > ON CONFLICT (id, valid_at) DO NOTHING
 > ON CONFLICT ON CONSTRAINT temporal_rng_pk DO NOTHING
 > I am confused by the test.
 > here temporal_rng only has one primary key, ON CONFLICT only deals with it.
 > I thought these three are the same thing?

They all have somewhat different code paths in infer_arbiter_indexes, and they mean different 
things. I recall when I first started dealing with empty ranges several of these test cases caught 
different bugs (as well as the DO UPDATE cases).

 > should this just be a static function?

Changed. In a previous version I was calling this from two places, but I'm not anymore.

 > char typtype

Oops, you're right! Fixed.

 > RelationGetRelationName(rel))

Yes, it's nicer to report the column name. Changed.

 > This part is unlikely to be reachable, so I don't have a strong opinion on it.

Likewise.

 > because if not found, the column would be last element in ColumnDef

We can later set `found` to true from inheritance (or it being a system column), and then `column` 
is set but wrong. So setting `column` to null seems generally clearer. But concretely, I use 
`column` below to give me the type (which I otherwise don't have in CREATE TABLE), so I can forbid 
types other than range and multirange.

 > + typid = typenameTypeId(NULL, column->typeName);

This is because in CREATE TABLE I need to get the type from the `column` variable.

 > + typid = attr->atttypid;

Yes. Actually that is in the PERIOD patch file, but it should be in Forbid-empty-ranges. Moved.

 > alter table t1 add CONSTRAINT temporal_rng_pk PRIMARY KEY (id, b

I think the same error message is the right thing to do here.
It looks like that's what we're doing.
If I've misunderstand what you want, can you clarify?

 > && strcmp(index->accessMethod, "gist") == 0);

This doesn't seem necessary, and maybe we'll support non-gist someday, when this condition would be 
misleading.

 > should be "if (pset.sversion >= 180000)"?

Ah, thanks. Changing these from 170000 also landed in the wrong patch file. Fixed.

Done.

 >                   errmsg("access method \"%s\" does not support WITHOUT

Okay.

 > supports exclusion constraint.

I think the comment is correct, but non-NIL is a confusing double negative, and it's not clear that 
the comment is giving the motivation for the second half of the condition.
I re-wrote it to be more clear. I also adjusted the `if` to avoid parsing operator names when not 
needed.

Rebased to e56ccc8e42.

Yours,

From 6fc665be618c4104f421c83c12847a02a7823f75 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 19 Jan 2024 15:41:44 +0100
Subject: [PATCH v39 1/8] Add stratnum GiST support function

 

 
 
 PG_MODULE_MAGIC;

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
 
 /*
 
 # gin
index d36f6001bb1..55a9e2d9cda 100644
@@ -12247,4 +12247,10 @@

From 04c245cbe7204a1f709fd9f7dc8031ac13bc42b7 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 24 Jan 2024 15:43:41 +0100
Subject: [PATCH v39 2/8] Add temporal PRIMARY KEY and UNIQUE constraints

 src/backend/commands/indexcmds.c              |  159 ++-
 33 files changed, 2598 insertions(+), 52 deletions(-)

 
 
+     <row>
 
  <variablelist>
 
+      <para>
index 93b3f664f21..8f5c72a4191 100644

 
     <listitem>
 
+     <para>
+      If the <literal>WITHOUT OVERLAPS</literal> option is specified for the
+      last column, then that column is checked for overlaps instead of
+      equality.  In that case, the other columns of the constraint will allow
+      duplicates so long as the duplicates don't overlap in the
+      <literal>WITHOUT OVERLAPS</literal> column.  (This is sometimes called a
+      temporal key, if the column is a range of dates or timestamps, but
+      <productname>PostgreSQL</productname> allows ranges over any base type.)
+      In effect, such a constraint is enforced with an <literal>EXCLUDE</literal>
+      constraint rather than a <literal>UNIQUE</literal> constraint.  So for
+      example <literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves
+      like <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
+      &amp;&amp;)</literal>.  The <literal>WITHOUT OVERLAPS</literal> column
+      must have a range or multirange type.  (Technically, any type is allowed
+      whose default GiST opclass includes an overlaps operator.  See the
+      <literal>stratnum</literal> support function under <xref
+      linkend="gist-extensibility"/> for details.)  The non-<literal>WITHOUT
+      OVERLAPS</literal> columns of the constraint can be any type that can be
+      compared for equality in a GiST index.  By default, only range types are
+      supported, but you can use other types by adding the <xref
+      linkend="btree-gist"/> extension (which is the expected way to use this
+      feature).
+     </para>
+
      <para>
       For the purpose of a unique constraint, null values are not
       considered equal, unless <literal>NULLS NOT DISTINCT</literal> is
@@ -1000,8 +1024,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
      <para>
 
      <para>

      <para>
 
 	PG_RETURN_UINT16(strat);
 }
+
+/*
 
 	pfree(ccbin);

 
 	/*
 
 	if (conkeyArray)
index c5a56c75f69..4d4dd0f61e6 100644

 
 
+	/*
 
-	if (stmt->unique && !amRoutine->amcanunique)
+	if (stmt->unique && !stmt->iswithoutoverlaps && !amRoutine->amcanunique)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support unique indexes",
@@ -863,11 +874,16 @@ DefineIndex(Oid tableId,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support multicolumn indexes",
 						accessMethodName)));
-	if (stmt->excludeOpNames && amRoutine->amgettuple == NULL)
+	if (exclusion && amRoutine->amgettuple == NULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support exclusion constraints",
 						accessMethodName)));
+	if (stmt->iswithoutoverlaps && strcmp(accessMethodName, "gist") != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("access method \"%s\" does not support WITHOUT OVERLAPS constraints",
+						accessMethodName)));
 
 	amcanorder = amRoutine->amcanorder;
 	amoptions = amRoutine->amoptions;
@@ -916,8 +932,9 @@ DefineIndex(Oid tableId,
 					  coloptions, allIndexParams,
 					  stmt->excludeOpNames, tableId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, stmt->isconstraint, root_save_userid,
-					  root_save_sec_context, &root_save_nestlevel);
+					  amcanorder, stmt->isconstraint, stmt->iswithoutoverlaps,
+					  root_save_userid, root_save_sec_context,
+					  &root_save_nestlevel);
 
 	/*
@@ -935,7 +952,7 @@ DefineIndex(Oid tableId,
@@ -989,10 +1006,10 @@ DefineIndex(Oid tableId,
@@ -1031,12 +1048,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -1045,7 +1062,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1189,6 +1206,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+	if (stmt->iswithoutoverlaps)
+		constr_flags |= INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1855,6 +1874,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool iswithoutoverlaps,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1878,6 +1898,25 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;
 
+	/*
+	 * If this is a WITHOUT OVERLAPS constraint,
+	 * we need space for exclusion ops,
+	 * but we don't need to parse anything,
+	 * so we can let nextExclOp be NULL.
+	 * Note that for partitions/inheriting/LIKE,
+	 * exclusionOpNames will be set, so we already allocated above.
+	 */
+	if (iswithoutoverlaps)
+	{
+		if (exclusionOpNames == NIL)
+		{
+			indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
+			indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
+			indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols);
+		}
+		nextExclOp = NULL;
+	}
+
 	if (OidIsValid(ddl_userid))
 		GetUserIdAndSecContext(&save_userid, &save_sec_context);
 
@@ -2157,6 +2196,21 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 
 		/*
 		 * Set up the per-column options (indoption field).  For now, this is
@@ -2387,6 +2441,83 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
 	return InvalidOid;
 }
 
+/*
index 1f94f4fdbbc..2b9dd4dc445 100644

 			/*

 
 
 	if (constrAddr)
 
 		relationDescs[i] = indexDesc;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 78a3cfafde4..5f16b26b4cc 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -829,7 +829,7 @@ infer_arbiter_indexes(PlannerInfo *root)
 		 */
 		if (indexOidFromConstraint == idxForm->indexrelid)
 		{
-			if (!idxForm->indisunique && onconflict->action == ONCONFLICT_UPDATE)
+			if (idxForm->indisexclusion && onconflict->action == ONCONFLICT_UPDATE)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 						 errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints")));
@@ -854,6 +854,13 @@ infer_arbiter_indexes(PlannerInfo *root)
 		if (!idxForm->indisunique)
 			goto next;
 
+		/*
 
+opt_without_overlaps:

 
+		/*
 
 	/*

 

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b6e01d3d292..eb3efa389a0 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7413,6 +7413,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conperiod,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7494,10 +7495,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7565,6 +7573,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7672,6 +7681,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -17103,6 +17113,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conperiod)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");

index 7c9a1f234c6..faabecbc76f 100644

 #include "access/itup.h"

 
+	/*
 
 	/*
 
 	/*

 

From e1a8f04ea5cccb849f011a9482f58c734bf5988c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 1 Jun 2024 15:24:56 -0700
Subject: [PATCH v39 3/8] Forbid empty ranges/multiranges in WITHOUT OVERLAPS
 columns

This all means we can only support ranges and multiranges for temporal
PK/UQs. So I added a check and updated the docs and tests.
---
 doc/src/sgml/gist.sgml                        |  3 -
 doc/src/sgml/ref/create_table.sgml            | 15 ++--
 src/backend/catalog/index.c                   | 10 ++-
 src/backend/commands/indexcmds.c              |  5 +-
 src/backend/executor/execIndexing.c           | 64 +++++++++++++++
 src/backend/nodes/makefuncs.c                 |  4 +-
 src/backend/parser/parse_utilcmd.c            | 48 ++++++++++-
 src/include/nodes/execnodes.h                 |  1 +
 src/include/nodes/makefuncs.h                 |  2 +-
 .../regress/expected/without_overlaps.out     | 80 ++++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 56 +++++++++++++
 11 files changed, 263 insertions(+), 25 deletions(-)

       <para>
index 8f5c72a4191..9243810c3fe 100644
       example <literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves
       like <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
 
      <para>
 
 	/*

 
-
 /*
index 4d4dd0f61e6..9af592f25d6 100644
@@ -920,7 +920,8 @@ DefineIndex(Oid tableId,
 
index 59acf67a36a..263980cd547 100644
 
 /* waitMode argument to check_exclusion_or_unique_constraint() */
@@ -141,6 +143,8 @@ static bool index_unchanged_by_update(ResultRelInfo *resultRelInfo,
 									  Relation indexRelation);
 static bool index_expression_changed_walker(Node *node,
 											Bitmapset *allUpdatedCols);
+static void ExecWithoutOverlapsNotEmpty(Relation rel, NameData attname, Datum attval,
+										char typtype, Oid atttypid);
 
 /* ----------------------------------------------------------------
  *		ExecOpenIndices
@@ -720,6 +724,32 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index,
 		constr_strats = indexInfo->ii_UniqueStrats;
 	}
 
+	/*
+			ExecWithoutOverlapsNotEmpty(heap, att->attname,
+										values[indnkeyatts - 1],
@@ -1097,3 +1127,37 @@ index_expression_changed_walker(Node *node, Bitmapset *allUpdatedCols)
+ExecWithoutOverlapsNotEmpty(Relation rel, NameData attname, Datum attval, char typtype, Oid atttypid)
+			elog(ERROR, "WITHOUT OVERLAPS column \"%s\" is not a range or multirange",
+				 NameStr(attname));
+				 errmsg("empty WITHOUT OVERLAPS value found in column \"%s\" in relation \"%s\"",
+						NameStr(attname), RelationGetRelationName(rel))));

index 53492c644a9..2a7f8fd154e 100644

 							/*
 							 * It's tempting to set forced_not_null if the
@@ -2524,6 +2530,46 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				}
 			}
 
index c3670f7158c..99d98e2de50 100644
 
index 19586c47b55..0fe3949f746 100644
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_rng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng3"
+ERROR:  empty WITHOUT OVERLAPS value found in column "valid_at" in relation "temporal_mltrng3"

 

 -- should fail:

 

 
 
 -- should fail:

-- 
2.42.0

From 561ef18f1776455c82c9ca34bb3e415d3ae34693 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 24 Mar 2024 07:37:13 +0100
Subject: [PATCH v39 4/8] Add temporal FOREIGN KEY contraints

 
index 9243810c3fe..9d552241a16 100644

 

+/*
+ * FindFkPeriodOpers -
+ *
index 9af592f25d6..3d51e4949eb 100644
@@ -2206,7 +2206,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
@@ -2446,7 +2446,7 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
@@ -2459,14 +2459,14 @@ GetDefaultOpClass(Oid type_id, Oid am_id)

 	*opid = InvalidOid;
 
@@ -2489,16 +2489,21 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 			ereport(ERROR,

 	if (!OidIsValid(*opid))
@@ -2511,9 +2516,9 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 		ereport(ERROR,
index 2b9dd4dc445..c52f3137261 100644

 
 				/*

+	/*
 
+	/*
 
-		/*
 
 		/*
 
+	/*

 	/*

 
 
 			/*

 

 

 	return i;
 
 		/*
 
 			/*
 
 	/*
 
 	/*

 

+optionalPeriodName:

 

 
+	/*
 
 	/*

 /*
- * ri_AttributesEqual -
+ * ri_CompareWithCast -
  *

 	/*
 
 /*

 

 

 
 	return nKeys;
 
 	/*

 	/*
 
 	/*
 
 	/*

 
index 0fe3949f746..86171c994c9 100644

 
+--
+-- test FK dependencies
+--
+

From 03dc9ec4ab8e4c2819910750a52490dd41db315b Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v39 5/8] Add support funcs for FOR PORTION OF

 doc/src/sgml/gist.sgml                        | 156 ++++++++++++++++-
 src/backend/access/gist/gistvalidate.c        |  31 ++--
 21 files changed, 680 insertions(+), 39 deletions(-)

index 638d912dc2d..d00df5c5fa2 100644
 
  <para>
 
  <variablelist>
@@ -1235,6 +1241,154 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15e0..487a57bf015 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
+        bounds (optional)</entry>
+       <literal>FOR PORTION OF</literal> bounds (optional)</entry>
index 0901543a60a..e1ad65b9a4f 100644
@@ -344,6 +353,8 @@ gistadjustmembers(Oid opfamilyoid,

 	va_start(ap, maxargs);
 
 /*
 
+/*
index 2d94a6b8774..cd0ffe231e3 100644

+ * RangeType pointers, already allocated with two elements.
 
 Datum

 /*
 
 # gin
index 55a9e2d9cda..7b21aba2bf1 100644
@@ -10643,6 +10643,10 @@
@@ -10930,6 +10934,10 @@

+-- without_portion

+-- without_portion

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

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f3eb055e2c7..48217bf6e78 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -6373,6 +6373,11 @@ DELETE FROM ft2
 
index 0734716ad90..bc325a1c3c6 100644
@@ -1579,6 +1579,10 @@ DELETE FROM ft2

 
 
+  <para>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>

index e1ad65b9a4f..d0886d91f07 100644

 
index c52f3137261..4d472212e11 100644

 

 

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

 /*

 

+	/*
index c6d18ae00f3..4dd0bf720b9 100644
@@ -314,7 +314,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan,
 
@@ -2834,6 +2834,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->onconflict,
 							best_path->mergeActionLists,
 							best_path->mergeJoinConditions,
+							best_path->forPortionOf,
 							best_path->epqParam);
 
@@ -7030,7 +7031,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
@@ -7096,6 +7097,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,

index e901203424d..1f5774324ba 100644
 
 #include "postgres.h"
 
@@ -47,10 +50,12 @@
 
@@ -59,10 +64,16 @@
 
@@ -482,6 +493,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -515,6 +540,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -553,7 +579,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -1189,7 +1219,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1219,6 +1249,187 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2426,6 +2637,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2443,6 +2655,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2459,7 +2675,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2469,7 +2686,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2488,7 +2705,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2541,6 +2758,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*

 

 					n->relation = $4;
-					n->usingClause = $5;
-					n->whereClause = $6;
-					n->returningList = $7;
+					n->forPortionOf = $5;
+					n->usingClause = $6;
+					n->whereClause = $7;
+					n->returningList = $8;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}

 					n->relation = $3;
-					n->targetList = $5;
-					n->fromClause = $6;
-					n->whereClause = $7;
-					n->returningList = $8;
+					n->forPortionOf = $4;
+					n->targetList = $6;
+					n->fromClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
 					n->withClause = $1;
 					$$ = (Node *) n;
 				}
 
+for_portion_of_clause:
 
 			/*
 
 			/*
index 56e413da9f5..c6fd437da32 100644
 
 			/*
@@ -1860,6 +1863,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 
 			/*
@@ -3153,6 +3159,8 @@ ParseExprKindName(ParseExprKind exprKind)
 
 			/*
index c223a2c50af..9de7c47f9c3 100644
@@ -3684,6 +3684,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
@@ -4023,6 +4047,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)

 /*

 

 

 	/*
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *

+/*
+ * get_typname
+ *

 #endif							/* STRATNUM_H */
 
 /*
index 99d98e2de50..2450a7702b7 100644

+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

 
+/*----------
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 86171c994c9..eccaf5c0f09 100644

 
 # ----------

 

 -- temporal UNIQUE:

 

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

---

index 9d552241a16..39e9cf8e5aa 100644
 
          <para>
 
          <para>
 
          <para>
 
 /*
- * FindFkPeriodOpers -
 
 /*
index 4d472212e11..d2df481663e 100644

 
-	/*

 	/*

 

+/*

 	ReleaseSysCache(tup);
 
 	/*

 
 
index 7b21aba2bf1..df3ef039078 100644

 
 explain (costs off)

 explain (costs off)
index eccaf5c0f09..71689fdbbc0 100644

 

+--
 
+--

 

 

 

 

 

 

 

 

 

-- 
2.42.0

From 1a38279f31d8c1498b7e0b1e4a1f9197b9474b75 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v39 8/8] Add PERIODs

 src/backend/parser/parse_utilcmd.c            |  179 +-
 src/bin/pg_dump/pg_dump.c                     |  175 +-
 59 files changed, 6293 insertions(+), 143 deletions(-)

+     <row>

 

index 39e9cf8e5aa..9abf89fd308 100644

 
+  <para>

 
      <para>
 
   <para>
 
   <para>
 
+					/*
 
+		case PeriodRelationId:
 
+/*

 /*
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case PeriodRelationId:
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index d2df481663e..e32efdf655b 100644

+	/*
 
+	/*
 
 	/*
 
+/*

+/*----------
+ * MergePeriods

 
 /*

 
+/*
 
+/*
 
-		if (attgenerated)
 
+			case PeriodRelationId:
 
 				/*

 

 
 		/*

 		/*
index 1f5774324ba..bdb253659b8 100644
@@ -1271,7 +1272,11 @@ transformForPortionOfClause(ParseState *pstate,
@@ -1315,6 +1320,53 @@ transformForPortionOfClause(ParseState *pstate,
 
+	/*
 
@@ -1382,7 +1434,10 @@ transformForPortionOfClause(ParseState *pstate,
@@ -1412,12 +1467,44 @@ transformForPortionOfClause(ParseState *pstate,

 
 
+
 TypedTableElement:

+TablePeriod:

index 2a7f8fd154e..f62a2c5b1ee 100644

+			case T_PeriodDef:

+void

 	/*
 
+		/*
 
@@ -2557,7 +2684,12 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
@@ -2908,6 +3040,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 		}
 	}
 
+	/* take care of the period */
+	if (stmt->period)
+		stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
 	/*
 	 * Check that only the base rel is mentioned.  (This should be dead code
 	 * now that add_missing_from is history.)
@@ -3519,6 +3655,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.inhRelations = NIL;
 	cxt.isalter = true;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
@@ -3579,6 +3716,22 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
+					/*

 /*
 
+Oid
 
 /*
index eb3efa389a0..ce635c14409 100644
@@ -6780,6 +6780,7 @@ getTables(Archive *fout, int *numTables)
@@ -6857,6 +6858,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6994,6 +7003,7 @@ getTables(Archive *fout, int *numTables)
@@ -7077,6 +7087,7 @@ getTables(Archive *fout, int *numTables)
@@ -8710,7 +8721,7 @@ getTransforms(Archive *fout)
@@ -8760,6 +8771,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8774,7 +8787,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9136,15 +9150,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9166,6 +9201,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9185,12 +9221,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9249,6 +9286,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10477,6 +10588,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16030,6 +16143,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16038,7 +16178,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16284,7 +16424,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 */
 			resetPQExpBuffer(extra);
 			firstitem = true;
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -16590,7 +16730,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18662,6 +18802,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:

index faabecbc76f..b1fb952a416 100644

 
+		/* print periods */

 
 
 /*

 

 #endif							/* TABLECMDS_H */

+
+/* ----------

 
 \d gtest1
                             Table "public.gtest1"
index 71689fdbbc0..0950738fe2c 100644

 

+--

 # ----------

+-- Test with PERIODs
+

 

+--

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;

 

---

+--

---

From: jian he <[email-masked]>
Date: Fri, 16 Aug 2024 10:12:00 +0800

> Rebased to e56ccc8e42.

I only applied to 0001-0003.
in create_table.sgml, I saw the WITHOUT OVERLAPS change is mainly in
table_constraint.
but we didn't touch alter_table.sgml.
Do we also need to change alter_table.sgml correspondingly?

if Constraint->conname is not NULL, we can
+ errmsg("constraint \"%s\" using WITHOUT OVERLAPS needs at least two
columns"));

"XXX Do we need this?"
I think currently we need this, otherwise the following create_table
synopsis will not be correct.
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] [,
column_name WITHOUT OVERLAPS ] )
PRIMARY KEY ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] )

we add a column in catalog-pg-constraint.
do we need change column conexclop,
"If an exclusion constraint, list of the per-column exclusion operators "
but currently, primary key, unique constraint both have valid conexclop.

+static void
+ExecWithoutOverlapsNotEmpty(Relation rel, NameData attname, Datum
attval, char typtype, Oid atttypid)
+{
+ bool isempty;
+ RangeType *r;
+ MultirangeType *mr;
+
+ switch (typtype)
+ {
+ case TYPTYPE_RANGE:
+ r = DatumGetRangeTypeP(attval);
+ isempty = RangeIsEmpty(r);
+ break;
+ case TYPTYPE_MULTIRANGE:
+ mr = DatumGetMultirangeTypeP(attval);
+ isempty = MultirangeIsEmpty(mr);
+ break;
+ default:
+ elog(ERROR, "WITHOUT OVERLAPS column \"%s\" is not a range or multirange",
+ NameStr(attname));
+ }
+
+ /* Report a CHECK_VIOLATION */
+ if (isempty)
+ ereport(ERROR,
+ (errcode(ERRCODE_CHECK_VIOLATION),
+ errmsg("empty WITHOUT OVERLAPS value found in column \"%s\" in
relation \"%s\"",
+ NameStr(attname), RelationGetRelationName(rel))));
+}
I think in the default branch, you need at least set the isempty
value, otherwise maybe there will be a compiler warning
because later your use isempty, but via default branch is value undefined?

+ /*
+ * If this is a WITHOUT OVERLAPS constraint,
+ * we must also forbid empty ranges/multiranges.
+ * This must happen before we look for NULLs below,
+ * or a UNIQUE constraint could insert an empty
+ * range along with a NULL scalar part.
+ */
+ if (indexInfo->ii_WithoutOverlaps)
+ {
+             ExecWithoutOverlapsNotEmpty(heap, att->attname,
+ }
previously we found out that if this happens later, then it won't work.
but this comment didn't explain why this must have happened earlier.
I didn't dig deep enough to find out why.
but explaining it would be very helpful.

I think some tests are duplicated, so I did the refactoring.

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 5 Sep 2024 14:09:49 +0200

On 07.08.24 22:54, Paul Jungwirth wrote:
> Here are some fixes based on outstanding feedback (some old some new). 

I have studied your patches v39-0001 through v39-0004, which correspond 
to what had been reverted plus the new empty range check plus various 
minor fixes.  This looks good to me now, so I propose to go ahead with that.

Btw., in your 0003 you point out that this prevents using the WITHOUT 
OVERLAPS functionality for non-range types.  But I think this could be 
accomplished by adding an "is empty" callback as a support function or 
something like that.  I'm not suggesting to do that here, but it might 
be worth leaving a comment about that possibility.

---

From: Paul A Jungwirth <[email-masked]>
Date: Thu, 5 Sep 2024 06:45:44 -0700

> minor fixes.  This looks good to me now, so I propose to go ahead with that.

Sounds good. Thanks!

> be worth leaving a comment about that possibility.

Yes, I was thinking the same. Agreed as well: it should be a follow-up
patch, not needed for the base functionality. If we wanted a more
generic name it could be "canWithoutOverlap" instead of "[!]isempty",
but even "isempty" is probably still completely accurate.

Yours,

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 17 Sep 2024 11:45:52 +0200

> be worth leaving a comment about that possibility.

I have committed these, as explained here.

I look forward to an updated patch set from you to review the "FOR 
PORTION OF" patches next.

---

From: Paul A Jungwirth <[email-masked]>
Date: Sun, 13 Oct 2024 00:25:40 -0500

> PORTION OF" patches next.

Here are updates to the remaining patches.

I made big changes to FOR PORTION OF:

- Inserting the leftovers now uses SPI. Before I tried to use just
ExecInsert with a patched-up mtstate and other things, but it felt
brittle and had some problems. FOR EACH STATEMENT insert triggers were
not firing, and the transition table for the ROW triggers contained
both inserted tuples. According to the spec, we should treat the two
leftover inserts as separate statements. For example on DELETE,
15.7.8.c.ii (for the leading leftover) and 8.d.ii (for the trailing
leftover) both say:

> The following <insert statement> is effectively executed . . .

So each side gets a *separate* insert statement.

DB2 also fires statement triggers (one for each insert) and gives them
separate transition tables. Mariadb doesn't have statement triggers,
but you can tell they also treat each insert as a separate statement,
because row triggers fire like this: BEFORE INSERT, AFTER INSERT,
BEFORE INSERT, AFTER INSERT. One statement for both inserts would be
BEFORE BEFORE AFTER AFTER.

Then there were other things that worked but at the cost of more code,
like partitioned tables. Using SPI seems much more robust. To help
performance I am caching plans much as we do in ri_triggers.c.

- I cleaned up how I was handling some shift/reduce conflicts. The
problem was how INTERVALs accept a TO qualifier to keep details above
a given precision. For example INTERVAL '1:05:03' HOUR TO MINUTE still
drops the 3 seconds but not the 5 minutes. So this was ambiguous: FOR
PORTION OF valid_at FROM t1 + INTERVAL '1:05:03' HOUR TO t2.
Previously I was solving this with extra %nonassoc declarations, with
a guilty conscience, but now I'm using %prec and adding just TO to
IDENT's %nonassoc list. This is much more in line with the comment in
gram.y's precedence section.

- FOR PORTION OF a FROM b TO c now appears before the [[AS]
table_alias], which is where the spec says it should be. (Previously I
had it after the table alias.) This led to 30 more shift/reduce
conflicts, but solving them was pretty easy once I realized SELECT
with *column* aliases must have all the same problems, and I noticed
our bare_label_keyword list. So I'm doing something similar: if there
is no FOR PORTION OF, all the same grammar rules are used. If you have
FOR PORTION OF, then we allow [AS table_alias] always and just
[table_alias] in most cases. This did require a couple more %precs and
adding USING to the IDENT precedence level.

I haven't yet dealt with jian he's latest patch feedback, but I
thought this was significant enough progress to be worth sharing.

I also did some experiments with logical replication, especially
around REPLICA IDENTITY. For a temporal table, you have a primary key
but (1) it is not btree (2) it doesn't use equality for the WITHOUT
OVERLAPS part. So I would not be surprised to find problems there.
OTOH in principle it should be okay. Remember that a temporal primary
key is *strictly more restrictive* than a PK (at least now that we've
forbidden empty ranges). If you have all the key parts, that *does*
give you a unique record (whether you use equals or overlaps for the
last part, actually).

Logical *decoding* works fine. For updates/deletes, we report all the
key parts of the old record. For instance suppose I do this:

create extension btree_gist;
create table t (id integer, valid_at daterange, primary key (id,
valid_at without overlaps));
insert into t values (1, '[2000-01-01,2020-01-01)');
update t set id = 2;
update t for portion of valid_at from '2001-01-01' to '2002-01-01' set id = 3;

Then `pg_recvlogical` shows me this:

...
BEGIN 21535
table public.t: INSERT: id[integer]:1
valid_at[daterange]:'[2000-01-01,2020-01-01)'
COMMIT 21535
BEGIN 21536
table public.t: UPDATE: old-key: id[integer]:1
valid_at[daterange]:'[2000-01-01,2020-01-01)' new-tuple: id[integer]:2
valid_at[daterange]:'[2000-01-01,2020-01-01)'
COMMIT 21536
BEGIN 21537
table public.t: UPDATE: old-key: id[integer]:2
valid_at[daterange]:'[2000-01-01,2020-01-01)' new-tuple: id[integer]:3
valid_at[daterange]:'[2001-01-01,2002-01-01)'
table public.t: INSERT: id[integer]:2
valid_at[daterange]:'[2000-01-01,2001-01-01)'
table public.t: INSERT: id[integer]:2
valid_at[daterange]:'[2002-01-01,2020-01-01)'
COMMIT 21537

I think that is exactly what we want. Even FOR PORTION OF
updates/deletes replicate nicely: we send the update/delete and then
we send the inserts for leftovers.

On the `subscription` side, we also get what we want *if* we use
REPLICA IDENTITY FULL.

But if we use `REPLICA IDENTITY DEFAULT` based on a WITHOUT OVERLAPS
primary key, the subscriber can't handle the GiST index. We get an
error like this:

2024-10-12 22:47:09.882 CDT [69308] ERROR:  missing operator 0(23,23)
in opfamily 16503
2024-10-12 22:47:09.882 CDT [69308] CONTEXT:  processing remote data
for replication origin "pg_17080" during message type "UPDATE" for
replication target relation "public.t" in transaction 21542, finished
at 0/11D9EEE0
2024-10-12 22:47:09.883 CDT [66619] LOG:  background worker "logical
replication apply worker" (PID 69308) exited with exit code 1
2024-10-12 22:47:14.874 CDT [69420] LOG:  logical replication apply
worker for subscription "s" has started

I will work on a fix for that, but I think for now we could also raise
an error on the publication side if you try to replicate
updates/deletes through a temporal PK, and then document that temporal
tables need to use `REPLICA IDENTITY FULL`.

I'll try to get to jian he's patch feedback soon as well.

And then there are a couple more bits of followup I'd like to do:

- Should we pass the FOR PORTION OF clause to FDWs? I think we should,
and it can be up to them whether they support it. But in the meantime
I think we can just say that FOR PORTION OF against FDWs is not
supported.

- We should allow the FOR PORTION OF FROM/TO bounds to be named
parameters. For instance you can't do this:

CREATE FUNCTION fpo_sql_update(target_from date, target_til date)
RETURNS VOID
AS $$
  UPDATE for_portion_of_test2
  FOR PORTION OF valid_at
    FROM target_from TO target_til
  SET name = concat(name, '*')
  WHERE id = '[1,2)';
$$
LANGUAGE sql;

The problem is that ColumnRefs aren't allowed as bounds, and that's
how we parse the parameters here. PL/pgSQL functions act the same. A
workaround is to use FROM $1 TO $2, which *does* work. Or of course
format + EXECUTE. Given those alternatives, this seems like a
low-priority problem, but I'm still interested in fixing it.

Actually I've already had at least one person ask for genuine
ColumnRefs in FOR PORTION OF bounds. According to the spec those
aren't allowed, and we currently evaluate them in ExecInitModifyTable,
but in principle I don't see any reason we couldn't permit them.
Perhaps we could even still evaluate them upfront if we can see they
are constant.

Rebased to c0b74323dc.

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Mon, 21 Oct 2024 14:46:14 -0700

Hello,

Here is a new set of patches, including new patches to (1) fix logical replication with WITHOUT 
OVERLAPS indexes and (2) address some documentation lapses pointed out in jian he's feedback. Since 
all that is against the already-commited PK/UNIQUE/FK work, I've kept them separate here from the 
FOR PORTION OF etc patches. I've also added the logical replication problem to the v18 Open Items 
wiki page.

Details below:

 > Do we also need to change alter_table.sgml correspondingly?

Good catch! Not just WITHOUT OVERLAPS but FOREIGN KEY needed to be updated too. Patched here.

 > PRIMARY KEY ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] )

Having zero scalar columns is not allowed according to SQL:2011, but supporting it seems like a cool 
thing to do eventually. It doesn't have to be in this first set of patches. But if we support it 
someday, I agree we must update these docs to match.

 > "If an exclusion constraint, list of the per-column exclusion operators"

I updated the docs to include the WITHOUT OVERLAPS case.

 > because later your use isempty, but via default branch is value undefined?

This isn't getting a warning on the build farm, so I think compilers are smart enough to see that 
elog exits.

 > but explaining it would be very helpful.

I don't follow. Explaining why it can't happen later is the same as explaining why it must happen 
sooner, right? But you say "must *have* happened earlier"---it's not that it must have happened 
already, but that we must do it now (before bailing when we look for NULLs).

 > I think some tests are duplicated, so I did the refactoring.

I kept some of this patch: querying tableoid::regclass from the partition root is nicer than 
querying the leaves separately.

The changes removing DELETEs & INSERTs made the tests less local, since they would depend on inserts 
made further up in the file. I'd rather keep the tests fairly isolated. I don't think this is the 
same thing as duplication.

Rebased to 68ad9816c1.

Yours,

From 9673c4e425ab4b23feb13efd02154648e5ff6a02 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:02:12 -0700
Subject: [PATCH v41 1/7] Add WITHOUT OVERLAPS and PERIOD to ALTER TABLE
 reference docs

We documented WITHOUT OVERLAPS in the CREATE TABLE docs, but not in
ALTER TABLE. Likewise foreign keys with PERIOD. This commit adds the
new syntax to ALTER TABLE.
---
 doc/src/sgml/ref/alter_table.sgml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 36770c012a6..d8fa86dba16 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -114,10 +114,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 
-- 
2.42.0

From d21256506a1f68d06cd61b0042a815a8ac3fbfbd Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:12:46 -0700
Subject: [PATCH v41 2/7] Update conexclop docs for WITHOUT OVERLAPS

---
 doc/src/sgml/catalogs.sgml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 964c819a02d..d2b559376f0 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2810,7 +2810,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        (references <link linkend="catalog-pg-operator"><structname>pg_operator</structname></link>.<structfield>oid</structfield>)
       </para>
       <para>
-       If an exclusion constraint, list of the per-column exclusion operators
+       If an exclusion constraint or <literal>WITHOUT OVERLAPS</literal>
+       primary key/unique constraint, list of the per-column exclusion operators.
       </para></entry>
      </row>
 
-- 
2.42.0

From 474447d0fdad2ac1e0889f1a9c2eeccb5adefb18 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 16 Oct 2024 11:06:40 -0700
Subject: [PATCH v41 3/7] Fix logical replication for temporal tables

A WITHOUT OVERLAPS primary key or unique constraint should work as a
REPLICA IDENTITY, since it guarantees uniqueness. But it is a GiST
index, not a btree, so logical replication was getting confused. To
determine the equals operator, we can use the stratnum GiST support
function.
---
 src/backend/commands/tablecmds.c              |  11 +-
 src/backend/executor/execReplication.c        |   9 +-
 .../regress/expected/without_overlaps.out     |  11 +-
 src/test/regress/sql/without_overlaps.sql     |   2 +-
 src/test/subscription/t/034_temporal.pl       | 668 ++++++++++++++++++
 5 files changed, 694 insertions(+), 7 deletions(-)
 create mode 100644 src/test/subscription/t/034_temporal.pl

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1ccc80087c3..ff4402a8bf3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16785,9 +16785,14 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 				 errmsg("\"%s\" is not an index for table \"%s\"",
 						RelationGetRelationName(indexRel),
 						RelationGetRelationName(rel))));
-	/* The AM must support uniqueness, and the index must in fact be unique. */
-	if (!indexRel->rd_indam->amcanunique ||
-		!indexRel->rd_index->indisunique)
+	/*
+	 * The AM must support uniqueness, and the index must in fact be unique.
+	 * If we have a WITHOUT OVERLAPS constraint (identified by uniqueness +
+	 * exclusion), we can use that too.
+	 */
+	if ((!indexRel->rd_indam->amcanunique ||
+		!indexRel->rd_index->indisunique) &&
+		!(indexRel->rd_index->indisunique && indexRel->rd_index->indisexclusion))
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("cannot use non-unique index \"%s\" as replica identity",
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 54025c9f150..152fe3140a0 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/genam.h"
+#include "access/gist.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
 #include "access/transam.h"
@@ -77,7 +78,11 @@ get_equal_strategy_number(Oid opclass)
 {
 	Oid			am = get_opclass_method(opclass);
 
-	return get_equal_strategy_number_for_am(am);
+	/* For GiST indexes we need to ask the opclass what strategy number to use. */
+	if (am == GIST_AM_OID)
+		return GistTranslateStratnum(opclass, RTEqualStrategyNumber);
+	else
+		return get_equal_strategy_number_for_am(am);
 }
 
 /*
@@ -134,6 +139,8 @@ build_replindex_scan_key(ScanKey skey, Relation rel, Relation idxrel,
 		optype = get_opclass_input_type(opclass->values[index_attoff]);
 		opfamily = get_opclass_family(opclass->values[index_attoff]);
 		eq_strategy = get_equal_strategy_number(opclass->values[index_attoff]);
+		if (!OidIsValid(eq_strategy))
+			elog(ERROR, "missing equal strategy for opclass %u", opclass->values[index_attoff]);
 
 		operator = get_opfamily_member(opfamily, optype,
 									   optype,
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 86171c994c9..a05b64e2f78 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -978,9 +978,16 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
 DROP TABLE temporal_partitioned;
 -- ALTER TABLE REPLICA IDENTITY
--- (should fail)
 ALTER TABLE temporal_rng REPLICA IDENTITY USING INDEX temporal_rng_pk;
-ERROR:  cannot use non-unique index "temporal_rng_pk" as replica identity
+\d temporal_rng
+              Table "public.temporal_rng"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           | not null | 
+ valid_at | daterange |           | not null | 
+Indexes:
+    "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) REPLICA IDENTITY
+
 --
 -- ON CONFLICT: ranges
 --
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 943edf3da63..ebee2f2932e 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -691,8 +691,8 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 DROP TABLE temporal_partitioned;
 
 -- ALTER TABLE REPLICA IDENTITY
--- (should fail)
 ALTER TABLE temporal_rng REPLICA IDENTITY USING INDEX temporal_rng_pk;
+\d temporal_rng
 
 --
 -- ON CONFLICT: ranges
diff --git a/src/test/subscription/t/034_temporal.pl b/src/test/subscription/t/034_temporal.pl
new file mode 100644
index 00000000000..3ef4249a9fe
--- /dev/null
+++ b/src/test/subscription/t/034_temporal.pl
@@ -0,0 +1,668 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Logical replication tests for temporal tables
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# setup
+
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init;
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+my ($result, $stdout, $stderr);
+
+# #################################
+# Test with REPLICA IDENTITY DEFAULT:
+# #################################
+
+# create tables on publisher
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text)"
+);
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+
+# create tables on subscriber
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text)"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+
+# sync initial data:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+$node_subscriber->wait_for_subscription_sync;
+
+$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},
+	'synced temporal_no_key DEFAULT');
+
+$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},
+	'synced temporal_pk DEFAULT');
+
+$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},
+	'synced temporal_unique DEFAULT');
+
+# replicate with no key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"UPDATE temporal_no_key SET a = 'b' WHERE id = '[2,3)'");
+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");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
+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");
+
+$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)|a
+[3,4)|[2000-01-01,2010-01-01)|a
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_no_key DEFAULT');
+
+# replicate with a primary key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+
+$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
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk DEFAULT');
+
+# replicate with a unique key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+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");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+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");
+
+$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)|a
+[3,4)|[2000-01-01,2010-01-01)|a
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique DEFAULT');
+
+# cleanup
+
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_no_key");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_no_key");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+
+# #################################
+# Test with REPLICA IDENTITY FULL:
+# #################################
+
+# create tables on publisher
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text)"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_no_key REPLICA IDENTITY FULL");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY FULL");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY FULL");
+
+# create tables on subscriber
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text)"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_no_key REPLICA IDENTITY FULL");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY FULL");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY FULL");
+
+# sync initial data:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+$node_subscriber->wait_for_subscription_sync;
+
+$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},
+	'synced temporal_no_key FULL');
+
+$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}, 'synced temporal_pk FULL');
+
+$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},
+	'synced temporal_unique FULL');
+
+# replicate with no key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_no_key SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
+
+$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
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_no_key FULL');
+
+# replicate with a primary key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+
+$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
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk FULL');
+
+# replicate with a unique key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+
+$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');
+
+# cleanup
+
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_no_key");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_no_key");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+
+# #################################
+# Test with REPLICA IDENTITY USING INDEX
+# #################################
+
+# create tables on publisher
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY USING INDEX temporal_pk_pkey");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY USING INDEX temporal_unique_id_valid_at_key"
+);
+
+# create tables on subscriber
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY USING INDEX temporal_pk_pkey");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range NOT NULL, valid_at daterange NOT NULL, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY USING INDEX temporal_unique_id_valid_at_key"
+);
+
+# sync initial data:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+$node_subscriber->wait_for_subscription_sync;
+
+$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},
+	'synced temporal_pk USING INDEX');
+
+$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},
+	'synced temporal_unique USING INDEX');
+
+# replicate with a primary key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+
+$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
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk USING INDEX');
+
+# replicate with a unique key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+
+$node_publisher->safe_psql('postgres',
+	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+
+$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 USING INDEX');
+
+# cleanup
+
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+
+# #################################
+# Test with REPLICA IDENTITY NOTHING
+# #################################
+
+# create tables on publisher
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_no_key REPLICA IDENTITY NOTHING");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY NOTHING");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range, valid_at daterange, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_publisher->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY NOTHING");
+
+# create tables on subscriber
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_no_key (id int4range, valid_at daterange, a text)"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_no_key REPLICA IDENTITY NOTHING");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_pk (id int4range, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_pk REPLICA IDENTITY NOTHING");
+
+$node_subscriber->safe_psql('postgres',
+	"CREATE TABLE temporal_unique (id int4range, valid_at daterange, a text, UNIQUE (id, valid_at WITHOUT OVERLAPS))"
+);
+$node_subscriber->safe_psql('postgres',
+	"ALTER TABLE temporal_unique REPLICA IDENTITY NOTHING");
+
+# sync initial data:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[1,2)', '[2000-01-01,2010-01-01)', 'a')");
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION pub1 FOR ALL TABLES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
+);
+$node_subscriber->wait_for_subscription_sync;
+
+$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},
+	'synced temporal_no_key NOTHING');
+
+$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},
+	'synced temporal_pk NOTHING');
+
+$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},
+	'synced temporal_unique NOTHING');
+
+# replicate with no key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_no_key (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"UPDATE temporal_no_key SET a = 'b' WHERE id = '[2,3)'");
+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");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
+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');
+
+$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)|a
+[3,4)|[2000-01-01,2010-01-01)|a
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_no_key NOTHING');
+
+# replicate with a primary key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_pk (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'");
+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");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
+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');
+
+$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)|a
+[3,4)|[2000-01-01,2010-01-01)|a
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk NOTHING');
+
+# replicate with a unique key:
+
+$node_publisher->safe_psql(
+	'postgres',
+	"INSERT INTO temporal_unique (id, valid_at, a)
+   VALUES ('[2,3)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[3,4)', '[2000-01-01,2010-01-01)', 'a'),
+          ('[4,5)', '[2000-01-01,2010-01-01)', 'a')");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'");
+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");
+
+($result, $stdout, $stderr) = $node_publisher->psql('postgres',
+	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
+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");
+
+$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)|a
+[3,4)|[2000-01-01,2010-01-01)|a
+[4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique NOTHING');
+
+# cleanup
+
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_publisher->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_pk");
+$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_unique");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+
+done_testing();
+
-- 
2.42.0

From 78a81cfdcfd1e9fbd3c3cb473b5f9cfc94780d10 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v41 4/7] Add support funcs for FOR PORTION OF

 src/backend/access/hash/hashvalidate.c        |   4 +-
 22 files changed, 682 insertions(+), 41 deletions(-)

index d2b559376f0..ddb40c00b91 100644
 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 66a3b7b49ad..3a8bd635b38 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -104,11 +104,11 @@ hashvalidate(Oid opclassoid)
 			case HASHSTANDARD_PROC:
 			case HASHEXTENDED_PROC:
-				ok = check_amproc_signature(procform->amproc, INT8OID, true,
+				ok = check_amproc_signature(procform->amproc, INT8OID, false, true,
 											2, 2, procform->amproclefttype, INT8OID);
 			case HASHOPTIONS_PROC:

 	va_start(ap, maxargs);
 
 /*
 
+/*

 
 Datum

 /*
index 5d7fe292bf6..7b9407543bb 100644
@@ -509,6 +509,9 @@
@@ -609,6 +612,12 @@
@@ -649,6 +658,12 @@
 
 # gin
index 7c0b74fe055..3ba69424682 100644
@@ -10732,6 +10732,10 @@
@@ -11019,6 +11023,10 @@

+-- without_portion

+-- without_portion

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

- Added bison support for FOR PORTION OF syntax. The bounds must be
  constant, so we forbid column references, subqueries, etc. But we
  permit the magic UNBOUNDED keyword as the FROM or the TO (or both)
  to perform an unbounded update/delete. We also accept functions like
  NOW().
- Added logic to executor to insert new rows for the "leftover" part of
  a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
  trigger functions. Our triggers use this to learn what bounds were
  given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   5 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   4 +
 contrib/sepgsql/proc.c                        |   2 +-
 contrib/unaccent/unaccent.c                   |   2 +-
 doc/src/sgml/catalogs.sgml                    |   4 +-
 doc/src/sgml/ref/delete.sgml                  |  72 +-
 doc/src/sgml/ref/update.sgml                  |  90 ++-
 doc/src/sgml/trigger.sgml                     |  64 +-
 src/backend/access/gist/gistvalidate.c        |  34 +-
 src/backend/commands/explain.c                |   2 +-
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/commands/trigger.c                |  46 ++
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/nodeModifyTable.c        | 497 +++++++++++-
 src/backend/executor/spi.c                    |   2 +-
 src/backend/nodes/nodeFuncs.c                 |  24 +
 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                  | 241 +++++-
 src/backend/parser/gram.y                     | 100 ++-
 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          |  40 +
 src/backend/utils/adt/ri_triggers.c           |  74 ++
 src/backend/utils/cache/lsyscache.c           |  61 +-
 src/include/commands/trigger.h                |   1 +
 src/include/nodes/execnodes.h                 |  25 +
 src/include/nodes/parsenodes.h                |  46 +-
 src/include/nodes/pathnodes.h                 |   1 +
 src/include/nodes/plannodes.h                 |   1 +
 src/include/nodes/primnodes.h                 |  26 +
 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/include/utils/lsyscache.h                 |   4 +-
 src/test/regress/expected/for_portion_of.out  | 753 ++++++++++++++++++
 src/test/regress/expected/opr_sanity.out      |   1 +
 src/test/regress/expected/privileges.out      |  18 +
 src/test/regress/expected/updatable_views.out |  32 +
 .../regress/expected/without_overlaps.out     | 186 ++++-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/for_portion_of.sql       | 591 ++++++++++++++
 src/test/regress/sql/opr_sanity.sql           |   1 +
 src/test/regress/sql/privileges.sql           |  18 +
 src/test/regress/sql/updatable_views.sql      |  14 +
 src/test/regress/sql/without_overlaps.sql     |  96 ++-
 src/test/subscription/t/034_temporal.pl       | 110 ++-
 52 files changed, 3218 insertions(+), 115 deletions(-)
 create mode 100644 src/test/regress/expected/for_portion_of.out
 create mode 100644 src/test/regress/sql/for_portion_of.sql

index f2bcd6aa98c..43be3ff9908 100644
 
index 372fe6dad15..786f57231a3 100644

 
 		dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ddb40c00b91..3d2a310e821 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -833,7 +833,7 @@
        <structfield>amoppurpose</structfield> <type>char</type>
       </para>
       <para>
-       Operator purpose, either <literal>s</literal> for search or
+       Operator purpose, either <literal>s</literal> for search, or
        <literal>o</literal> for ordering
       </para></entry>
      </row>
@@ -865,7 +865,7 @@
       </para>
       <para>
        The B-tree operator family this entry sorts according to, if an
-       ordering operator; zero if a search operator or portion operator
+       ordering operator; zero if a search operator
       </para></entry>
      </row>
     </tbody>
diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 7717855bc9e..6330fdff4c0 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -22,7 +22,9 @@ 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_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</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 { * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
@@ -54,6 +56,39 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
@@ -116,6 +151,41 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><replaceable class="parameter">range_or_period_name</replaceable></term>
+    <listitem>
+     <para>
+      The range column or period to use when performing a temporal delete.
+     </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 or period from
+      <replaceable class="parameter">range_or_period_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 or period from
+      <replaceable class="parameter">range_or_period_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>
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 1c433bec2bb..c893cc795f2 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_or_period_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> )
@@ -51,6 +53,41 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
@@ -114,6 +151,57 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>
index a9abaab9056..e4f63075a36 100644
@@ -366,6 +366,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>
@@ -547,17 +556,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>
 
@@ -825,6 +835,38 @@ 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 from FOR PORTION OF */
+} 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>

 
 	/* Now check for inconsistent groups of operators/functions */
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 18a5af6b919..5439adc0d28 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4396,7 +4396,7 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 
index ff4402a8bf3..fd2c8df56d6 100644
@@ -12323,6 +12323,7 @@ validateForeignKeyConstraint(char *conname,

index 09356e46d16..55a25a36c8b 100644

 

@@ -4120,6 +4129,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
@@ -4488,6 +4498,7 @@ AfterTriggerExecute(EState *estate,

@@ -6022,6 +6033,40 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6437,6 +6482,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 
index cc9a594cba5..561ac69fc74 100644
@@ -1252,6 +1252,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index 1161520f76b..9f2234320f6 100644
@@ -52,6 +52,7 @@
 
 #include "postgres.h"
 
+#include "access/heapam.h"
 #include "access/htup_details.h"
 #include "access/tableam.h"
 #include "access/xact.h"
@@ -59,14 +60,18 @@
 #include "executor/execPartition.h"
 #include "executor/executor.h"
 #include "executor/nodeModifyTable.h"
+#include "executor/spi.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/lmgr.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 
@@ -123,6 +128,19 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
+/*
+ * FPO_QueryHashEntry
+ */
+typedef struct FPO_QueryHashEntry {
+	Oid			key;
+	SPIPlanPtr	plan;
+} FPO_QueryHashEntry;
+
+/*
+ * Plan cache for FOR PORTION OF inserts
+ */
+#define FPO_INIT_QUERYHASHSIZE 32
+static HTAB *fpo_query_cache = NULL;
 
 static void ExecBatchInsert(ModifyTableState *mtstate,
 							ResultRelInfo *resultRelInfo,
@@ -144,6 +162,13 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
 								 TupleTableSlot *excludedSlot,
 								 bool canSetTag,
 								 TupleTableSlot **returning);
+static void fpo_InitHashTable(void);
+static SPIPlanPtr fpo_FetchPreparedPlan(Oid relid);
+static void fpo_HashPreparedPlan(Oid relid, SPIPlanPtr plan);
+static void ExecForPortionOfLeftovers(ModifyTableContext *context,
+									  EState *estate,
+									  ResultRelInfo *resultRelInfo,
+									  ItemPointer tupleid);
 static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
 											   EState *estate,
 											   PartitionTupleRouting *proute,
@@ -166,6 +191,7 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
 static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
 										   ResultRelInfo *resultRelInfo,
 										   bool canSetTag);
+static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate);

 /*
@@ -1214,6 +1240,367 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+/* ----------------------------------------------------------------
+ *		fpo_InitHashTable
+ *
+ *		Creates a hash table to hold SPI plans to insert leftovers
+ *		from a PORTION OF UPDATE/DELETE
+ * ----------------------------------------------------------------
+ */
+static void
+fpo_InitHashTable(void)
+{
+	HASHCTL		ctl;
+
+	ctl.keysize = sizeof(Oid);
+	ctl.entrysize = sizeof(FPO_QueryHashEntry);
+	fpo_query_cache = hash_create("FPO_query_cache",
+								  FPO_INIT_QUERYHASHSIZE,
+								  &ctl, HASH_ELEM | HASH_BLOBS);
+}
+
+/* ----------------------------------------------------------------
+ *		fpo_FetchPreparedPlan
+ *
+ *		Lookup for a query key in our private hash table of
+ *		prepared and saved SPI execution plans. Returns the plan
+ *		if found or NULL.
+ * ----------------------------------------------------------------
+ */
+static SPIPlanPtr
+fpo_FetchPreparedPlan(Oid relid)
+{
+	FPO_QueryHashEntry *entry;
+	SPIPlanPtr	plan;
+
+	if (!fpo_query_cache)
+		fpo_InitHashTable();
+
+	/*
+	 * Lookup for the key
+	 */
+	entry = (FPO_QueryHashEntry *) hash_search(fpo_query_cache,
+											   &relid,
+											   HASH_FIND, NULL);
+
+	if (!entry)
+		return NULL;
+
+	/*
+	 * Check whether the plan is still valid.  If it isn't, we don't want to
+	 * simply rely on plancache.c to regenerate it; rather we should start
+	 * from scratch and rebuild the query text too.  This is to cover cases
+	 * such as table/column renames.  We depend on the plancache machinery to
+	 * detect possible invalidations, though.
+	 *
+	 * CAUTION: this check is only trustworthy if the caller has already
+	 * locked both FK and PK rels.
+	 */
+	plan = entry->plan;
+	if (plan && SPI_plan_is_valid(plan))
+		return plan;
+
+	/*
+	 * Otherwise we might as well flush the cached plan now, to free a little
+	 * memory space before we make a new one.
+	 */
+	entry->plan = NULL;
+	if (plan)
+		SPI_freeplan(plan);
+
+	return NULL;
+}
+
+/* ----------------------------------------------------------------
+ *		fpo_HashPreparedPlan
+ *
+ *		Add another plan to our private SPI query plan hashtable.
+ * ----------------------------------------------------------------
+ */
+static void
+fpo_HashPreparedPlan(Oid relid, SPIPlanPtr plan)
+{
+	FPO_QueryHashEntry *entry;
+	bool		found;
+
+	/*
+	 * On the first call initialize the hashtable
+	 */
+	if (!fpo_query_cache)
+		fpo_InitHashTable();
+
+	/*
+	 * Add the new plan. We might be overwriting an entry previously found
+	 * invalid by fpo_FetchPreparedPlan.
+	 */
+	entry = (FPO_QueryHashEntry *) hash_search(fpo_query_cache,
+											   &relid,
+											   HASH_ENTER, &found);
+	Assert(!found || entry->plan == NULL);
+	entry->plan = plan;
+}
+
+/* ----------------------------------------------------------------
+ *		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;
+	Oid	relid;
+	TupleDesc tupdesc;
+	int	natts;
+	Datum	oldRange;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+	TupleTableSlot *leftoverSlot;
+	TupleConversionMap *map = NULL;
+	HeapTuple oldtuple = NULL;
+	FmgrInfo flinfo;
+	ReturnSetInfo rsi;
+	Relation rel;
+	bool didInit = false;
+	bool shouldFree = false;
+	LOCAL_FCINFO(fcinfo, 2);
+
+	/*
+	 * Get the range of the old pre-UPDATE/DELETE tuple,
+	 * so we can intersect it with the FOR PORTION OF target
+	 * and see if there are any "leftovers" to insert.
+	 *
+	 * We have already locked the tuple in ExecUpdate/ExecDelete
+	 * and it has passed EvalPlanQual.
+	 * Make sure we're looking at the most recent version.
+	 * Otherwise concurrent updates of the same tuple in READ COMMITTED
+	 * could insert conflicting "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.
+	 */
+
+	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 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;
+
+	rel = resultRelInfo->ri_RelationDesc;
+	relid = RelationGetRelid(rel);
+
+	/* Insert a leftover for each value returned by the without_portion helper function */
+	while (true)
+	{
+		Datum leftover = FunctionCallInvoke(fcinfo);
+		SPIPlanPtr	qplan = NULL;
+		int spi_result;
+
+		/* Are we done? */
+		if (rsi.isDone == ExprEndResult)
+			break;
+
+		if (fcinfo->isnull)
+			elog(ERROR, "Got a null from without_portion function");
+
+		if (!didInit)
+		{
+			/*
+			 * Convert oldtuple to the base table's format if necessary.
+			 * We need to insert leftovers through the root partition
+			 * so they get routed correctly.
+			 */
+			if (map != NULL)
+			{
+				leftoverSlot = execute_attr_map_slot(map->attrMap,
+													 oldtupleSlot,
+													 fpoState->fp_Leftover);
+			}
+			else
+				leftoverSlot = oldtupleSlot;
+
+			tupdesc = leftoverSlot->tts_tupleDescriptor;
+			natts = tupdesc->natts;
+
+			/*
+			 * If targeting a leaf partition,
+			 * it may not have fp_values/fp_nulls yet.
+			 */
+			if (!fpoState->fp_values)
+			{
+				fpoState->fp_values = palloc(natts * sizeof(Datum));
+				fpoState->fp_nulls = palloc(natts * sizeof(char));
+			}
+
+			/*
+			 * Copy (potentially mapped) oldtuple values into SPI input arrays.
+			 * We'll overwrite the range/start/end attributes below.
+			 */
+			memcpy(fpoState->fp_values, leftoverSlot->tts_values, natts * sizeof(Datum));
+
+			SPI_connect();
+
+			didInit = true;
+		}
+
+		/*
+		 * Build an SPI plan if we don't have one yet.
+		 * We always insert into the root partition,
+		 * so that we get tuple routing.
+		 * Therefore the plan is the same no matter which leaf
+		 * we are updating/deleting.
+		 */
+		if (!qplan && !(qplan = fpo_FetchPreparedPlan(relid)))
+		{
+			Oid	*types = palloc0(natts * sizeof(Oid));
+			int i;
+			bool started = false;
+			StringInfoData querybuf;
+			StringInfoData parambuf;
+			const char *tablename;
+			const char *schemaname;
+			const char *colname;
+
+			initStringInfo(&querybuf);
+			initStringInfo(&parambuf);
+
+			schemaname = get_namespace_name(RelationGetNamespace(rel));
+			tablename = RelationGetRelationName(rel);
+			appendStringInfo(&querybuf, "INSERT INTO %s (",
+							 quote_qualified_identifier(schemaname, tablename));
+
+			for (i = 0; i < natts; i++) {
+				/* Don't try to insert into dropped or generated columns */
+				if (tupdesc->attrs[i].attisdropped || tupdesc->attrs[i].attgenerated)
+					continue;
+
+				types[i] = tupdesc->attrs[i].atttypid;
+
+				colname = quote_identifier(NameStr(*attnumAttName(rel, i + 1)));
+				if (started)
+				{
+					appendStringInfo(&querybuf, ", %s", colname);
+					appendStringInfo(&parambuf, ", $%d", i + 1);
+				}
+				else
+				{
+					appendStringInfo(&querybuf, "%s", colname);
+					appendStringInfo(&parambuf, "$%d", i + 1);
+				}
+				started = true;
+			}
+			appendStringInfo(&querybuf, ") VALUES (%s)", parambuf.data);
+
+			qplan = SPI_prepare(querybuf.data, natts, types);
+			if (!qplan)
+				elog(ERROR, "SPI_prepare returned %s for %s",
+						SPI_result_code_string(SPI_result), querybuf.data);
+
+			SPI_keepplan(qplan);
+			fpo_HashPreparedPlan(relid, qplan);
+		}
+
+		/*
+		 * Set up the SPI params.
+		 * Copy most attributes' old values,
+		 * but for the range/start/end use the leftover.
+		 */
+
+		/* Convert bool null array to SPI char array */
+		for (int i = 0; i < natts; i++)
+		{
+			/*
+			 * Don't try to insert into dropped or generated columns.
+			 * Tell SPI these params are null just to be safe.
+			 */
+			if (tupdesc->attrs[i].attisdropped || tupdesc->attrs[i].attgenerated)
+				fpoState->fp_nulls[i] = 'n';
+			else
+				fpoState->fp_nulls[i] = leftoverSlot->tts_isnull[i] ? 'n' : ' ';
+		}
+
+		fpoState->fp_nulls[forPortionOf->rangeVar->varattno - 1] = ' ';
+		fpoState->fp_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+
+		spi_result = SPI_execute_snapshot(qplan,
+										  fpoState->fp_values,
+										  fpoState->fp_nulls,
+										  GetLatestSnapshot(),
+										  InvalidSnapshot, false, true, 0);
+		if (spi_result != SPI_OK_INSERT)
+			elog(ERROR, "SPI_execute_snapshot returned %s", SPI_result_code_string(spi_result));
+	}
+
+	if (didInit)
+	{
+		if (SPI_finish() != SPI_OK_FINISH)
+			elog(ERROR, "SPI_finish failed");
+
+		if (shouldFree)
+			heap_freetuple(oldtuple);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1366,7 +1753,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 leftovers from a FOR PORTION OF
+ * delete.
  */
 static void
 ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1399,6 +1787,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
@@ -1779,7 +2171,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2145,6 +2541,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4640,6 +5040,99 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+	 * If needed, initialize the target range for FOR PORTION OF.
+	 */
+	if (node->forPortionOf)
+	{
+		ResultRelInfo *rootResultRelInfo;
+		TupleDesc tupDesc;
+		ForPortionOfExpr *forPortionOf;
+		Datum	targetRange;
+		bool	isNull;
+		ExprContext *econtext;
+		ExprState *exprState;
+		ForPortionOfState *fpoState;
+
+		rootResultRelInfo = mtstate->resultRelInfo;
+		if (rootResultRelInfo->ri_RootResultRelInfo)
+			rootResultRelInfo = rootResultRelInfo->ri_RootResultRelInfo;
+
+		tupDesc = rootResultRelInfo->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 range");
+
+		/* 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(rootResultRelInfo->ri_RelationDesc,
+							  &mtstate->ps.state->es_tupleTable);
+
+		/* Create the tuple slot for INSERTing the leftovers */
+
+		fpoState->fp_Leftover =
+			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+
+		/* Allocate our SPI param arrays here so we can reuse them */
+		fpoState->fp_values = palloc(tupDesc->natts * sizeof(Datum));
+		fpoState->fp_nulls = palloc(tupDesc->natts * sizeof(char));
+
+		/*
+		 * We must attach the ForPortionOfState to all result rels,
+		 * in case of a cross-partition update or triggers firing
+		 * on partitions.
+		 * XXX: Can we defer this to only the leafs we touch?
+		 */
+		for (i = 0; i < nrels; i++)
+		{
+			ForPortionOfState	*leafState;
+			resultRelInfo = &mtstate->resultRelInfo[i];
+
+			leafState = makeNode(ForPortionOfState);
+			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);
+			/*
+			 * Leafs need them own SPI input arrays
+			 * since they might have extra attributes,
+			 * but we'll allocate those as needed.
+			 */
+			leafState->fp_values = NULL;
+			leafState->fp_nulls = NULL;
+
+			resultRelInfo->ri_forPortionOf = leafState;
+		}
+
+		/* Make sure the root relation has the FOR PORTION OF clause too. */
+		if (node->rootRelation > 0)
+			mtstate->rootResultRelInfo->ri_forPortionOf = fpoState;
+
+		/* 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/executor/spi.c b/src/backend/executor/spi.c
index 2fb2e73604e..b07d88ce2fe 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -764,7 +764,7 @@ SPI_execute_plan_with_paramlist(SPIPlanPtr plan, ParamListInfo params,
  * end of the command.
  *
  * This is currently not documented in spi.sgml because it is only intended
- * for use by RI triggers.
+ * for use by RI triggers and FOR PORTION OF.
  *
  * Passing snapshot == InvalidSnapshot will select the normal behavior of
  * fetching a new snapshot for each query.
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index f76072228c9..95011120495 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2561,6 +2561,14 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_ForPortionOfExpr:
+			{
+				ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node;
+
+				if (WALK(forPortionOf->targetRange))
+					return true;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -2707,6 +2715,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))
@@ -3591,6 +3601,19 @@ 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->targetRange, fpo->targetRange, Node *);
+				MUTATE(newnode->rangeTargetList, fpo->rangeTargetList, List *);
+
+				return (Node *) newnode;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3772,6 +3795,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 f2ed0d81f61..200d2989f1a 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -316,7 +316,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);
 
@@ -2838,6 +2838,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->onconflict,
 							best_path->mergeActionLists,
 							best_path->mergeJoinConditions,
+							best_path->forPortionOf,
 							best_path->epqParam);
 
@@ -7111,7 +7112,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
@@ -7177,6 +7178,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
index 0f423e96847..eda2b1cf45d 100644
@@ -2048,6 +2048,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 
index fc97bf6ee26..a4cc6e09620 100644
@@ -3827,7 +3827,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3894,6 +3894,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
index 8a6ba1692e8..8610ab403bd 100644
 
 #include "postgres.h"

@@ -59,10 +64,16 @@

+static Node *

 
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1219,6 +1249,186 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2427,6 +2637,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2444,6 +2655,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2460,7 +2675,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2470,7 +2686,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2489,7 +2705,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2542,6 +2758,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 4aa8646af7b..eced82efd23 100644
@@ -553,6 +554,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+%type <alias>	opt_alias
 
@@ -762,7 +765,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
 
@@ -881,12 +884,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		'*' '/' '%'
@@ -12325,6 +12331,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 = $5;
+					n->relation->alias = $6;
+					n->usingClause = $7;
+					n->whereClause = $8;
+					n->returningList = $9;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 using_clause:
@@ -12399,6 +12419,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 = $4;
+					n->relation->alias = $5;
+					n->targetList = $7;
+					n->fromClause = $8;
+					n->whereClause = $9;
+					n->returningList = $10;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 set_clause_list:
@@ -13864,6 +13903,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;
+					$$ = 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;
+				}
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -14697,17 +14774,26 @@ 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
-				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); }
+				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); }	%prec IS
 			| interval_second
 				{ $$ = $1; }
 			| YEAR_P TO MONTH_P
@@ -17757,6 +17843,7 @@ unreserved_keyword:
 			| PLAN
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -18382,6 +18469,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 efa730c1676..53985f74df5 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -579,6 +579,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 		case EXPR_KIND_CYCLE_MARK:
 			errkind = true;
 			break;
+		case EXPR_KIND_UPDATE_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;
 
 			/*
@@ -970,6 +977,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
index ef0b560f5e3..0f2165c4702 100644
 
 			/*
 
 			/*
 
 			/*
index 6d59a2bb8dc..e9f64e74bc2 100644
 
index 6896e1ae638..297fba71bb5 100644

 /*
@@ -449,6 +455,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -613,6 +620,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,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -712,6 +720,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
 
@@ -801,9 +811,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -909,6 +926,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1029,6 +1047,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1260,6 +1279,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,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2405,6 +2425,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2460,6 +2481,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3136,3 +3163,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index a85dc0d891f..254731830ba 100644
 
@@ -1643,8 +1643,10 @@ get_func_namespace(Oid funcid)
 		ReleaseSysCache(tp);
 		return result;
 	}
-	else
-		return InvalidOid;
+	else if (!missing_ok)
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+
+	return InvalidOid;
 }
 
 /*
 
+/*
+ * get_typname
+ *
 
 /*
index e4698a28c4f..8da5618ee19 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -431,6 +433,26 @@ 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 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 */
+	Datum  *fp_values;	/* SPI input for leftover values */
+	char   *fp_nulls;	/* SPI input for nulls */
+} ForPortionOfState;
+
 /*
  * ResultRelInfo
  *
@@ -558,6 +580,9 @@ typedef struct ResultRelInfo
 	/* for MERGE, expr state for checking the join condition */
 	ExprState  *ri_MergeJoinCondition;

index c92cef3d16d..e86c3a5a0ed 100644
 
@@ -1582,6 +1585,21 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -2054,12 +2072,13 @@ typedef struct InsertStmt
 
@@ -2068,13 +2087,14 @@ typedef struct DeleteStmt
 
 /* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 07e2415398e..d538ae23808 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2392,6 +2392,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 52f29bcdb69..73f3171cddb 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -244,6 +244,7 @@ typedef struct ModifyTable
 	List	   *rowMarks;		/* PlanRowMarks (non-locking only) */
 	int			epqParam;		/* ID of Param for EvalPlanQual re-eval */
 	OnConflictAction onConflictAction;	/* ON CONFLICT action */
+	Node	   *forPortionOf;	/* FOR PORTION OF clause for UPDATE/DELETE */
 	List	   *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs  */
 	List	   *onConflictSet;	/* INSERT ON CONFLICT DO UPDATE targetlist */
 	List	   *onConflictCols; /* target column numbers for onConflictSet */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index b0ef1952e8f..8392018a821 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2336,4 +2336,30 @@ 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.
+ *
+ * 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	   *targetRange;		/* FOR PORTION OF bounds as a range */
+	Oid			rangeType;			/* type of targetRange */
+	Node	   *overlapsExpr;		/* range && targetRange */
+	List	   *rangeTargetList;	/* List of TargetEntrys to set the time column(s) */
+	Oid			withoutPortionProc;	/* 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 1035e6560c1..b594cccab92 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -289,7 +289,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 28b66fccb43..6c70dae1452 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 List *transformReturningList(ParseState *pstate, List *returningList,
 									ParseExprKind exprKind);
 extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 899d64ad55f..425987afee7 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -343,6 +343,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 543df568147..8c273da94e7 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_UPDATE_PORTION,	/* UPDATE 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/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 20446f6f836..e7ad513146a 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -121,7 +121,7 @@ extern Oid	get_negator(Oid opno);
 extern RegProcedure get_oprrest(Oid opno);
 extern RegProcedure get_oprjoin(Oid opno);
 extern char *get_func_name(Oid funcid);
-extern Oid	get_func_namespace(Oid funcid);
+extern Oid	get_func_namespace(Oid funcid, bool missing_ok);
 extern Oid	get_func_rettype(Oid funcid);
 extern int	get_func_nargs(Oid funcid);
 extern Oid	get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
@@ -145,6 +145,8 @@ extern Oid	get_rel_relam(Oid relid);
 extern Oid	get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
 extern Oid	get_transform_tosql(Oid typid, Oid langid, List *trftypes);
 extern bool get_typisdefined(Oid typid);
+extern char *get_typname(Oid typid);
+extern bool get_typname_and_namespace(Oid typid, char **typname, char **typnamespace);
 extern int16 get_typlen(Oid typid);
 extern bool get_typbyval(Oid typid);
 extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
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..db217c61d2f
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,753 @@
+-- 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 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';
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20';
+-- 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 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 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;
+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
+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')
+;
+--
+-- UPDATE tests
+--
+-- Setting 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
+                       ^
+-- Setting 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:  can't directly assign to "valid_at" in a FOR PORTION OF update
+LINE 3: SET valid_at = '[1990-01-01,1999-01-01)'
+            ^
+-- The wrong type fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 1 TO 4
+SET name = 'nope'
+WHERE id = '[3,4)';
+ERROR:  function pg_catalog.daterange(integer, integer) does not exist
+LINE 2: FOR PORTION OF valid_at FROM 1 TO 4
+                       ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- Setting 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
+-- Setting 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 '2018-...
+                                     ^
+-- Setting 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
+                                           ^
+-- Setting 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- 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)';
+-- With a direct target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-17'))
+SET name = 'one^3'
+WHERE id = '[1,2)';
+-- 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 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;
+  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-17) | one^3
+ [6,7) | [2018-03-17,2018-04-04) | one^2
+(20 rows)
+
+-- 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;
+--
+-- DELETE tests
+--
+-- 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
+                       ^
+-- 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 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 with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO NULL
+WHERE id = '[3,4)';
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO '2018-02-08'
+WHERE id = '[1,2)';
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM NULL TO NULL
+WHERE id = '[6,7)';
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO NULL;
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [2018-02-08,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-04-01) | three
+ [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,2018-06-01) | five^2
+ [5,6) | [2020-06-01,2025-01-01) | five
+(11 rows)
+
+-- 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)
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+  RAISE NOTICE '% % % % of %', TG_WHEN, TG_OP, TG_LEVEL, NEW.valid_at, OLD.valid_at;
+  IF TG_OP = 'DELETE' THEN
+    RETURN OLD;
+  ELSE
+    RETURN NEW;
+  END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER trg_for_portion_of_before
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_stmt
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_stmt
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test
+  FOR EACH STATEMENT
+  EXECUTE FUNCTION for_portion_of_trigger();
+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:  BEFORE UPDATE STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE UPDATE ROW [2021-01-01,2022-01-01) of [2020-06-01,2025-01-01)
+NOTICE:  BEFORE INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT ROW [2020-06-01,2021-01-01) of <NULL>
+NOTICE:  AFTER INSERT ROW [2020-06-01,2021-01-01) of <NULL>
+NOTICE:  AFTER INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT ROW [2022-01-01,2025-01-01) of <NULL>
+NOTICE:  AFTER INSERT ROW [2022-01-01,2025-01-01) of <NULL>
+NOTICE:  AFTER INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  AFTER UPDATE ROW [2021-01-01,2022-01-01) of [2020-06-01,2025-01-01)
+NOTICE:  AFTER UPDATE STATEMENT <NULL> of <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:  BEFORE DELETE STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE DELETE ROW <NULL> of [2022-01-01,2025-01-01)
+NOTICE:  BEFORE INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT ROW [2022-01-01,2023-01-01) of <NULL>
+NOTICE:  AFTER INSERT ROW [2022-01-01,2023-01-01) of <NULL>
+NOTICE:  AFTER INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  BEFORE INSERT ROW [2024-01-01,2025-01-01) of <NULL>
+NOTICE:  AFTER INSERT ROW [2024-01-01,2025-01-01) of <NULL>
+NOTICE:  AFTER INSERT STATEMENT <NULL> of <NULL>
+NOTICE:  AFTER DELETE ROW <NULL> of [2022-01-01,2025-01-01)
+NOTICE:  AFTER DELETE STATEMENT <NULL> of <NULL>
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+  id   |        valid_at         |  name   
+-------+-------------------------+---------
+ [1,2) | [2018-02-08,2018-02-15) | one^2
+ [2,3) | [2018-01-01,2018-01-05) | two^2
+ [3,4) | [2018-01-01,2018-02-01) | three^2
+ [3,4) | [2018-02-01,2018-02-15) | three^3
+ [3,4) | [2018-02-15,2018-03-01) | three^2
+ [3,4) | [2018-03-01,2018-04-01) | three
+ [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,2018-06-01) | five^2
+ [5,6) | [2020-06-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,2025-01-01) | five
+(16 rows)
+
+DROP FUNCTION for_portion_of_trigger CASCADE;
+NOTICE:  drop cascades to 4 other objects
+DETAIL:  drop cascades to trigger trg_for_portion_of_before on table for_portion_of_test
+drop cascades to trigger trg_for_portion_of_after on table for_portion_of_test
+drop cascades to trigger trg_for_portion_of_before_stmt on table for_portion_of_test
+drop cascades to trigger trg_for_portion_of_after_stmt on table for_portion_of_test
+-- 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');
+CREATE FUNCTION dump_trigger()
+RETURNS TRIGGER LANGUAGE plpgsql AS
+$$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    RAISE NOTICE '%: % %, NEW table = %',
+      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'UPDATE' THEN
+    RAISE NOTICE '%: % %, OLD table = %, NEW table = %',
+      TG_NAME, TG_OP, TG_LEVEL,
+      (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table),
+      (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+  ELSIF TG_OP = 'DELETE' THEN
+    RAISE NOTICE '%: % %, OLD table = %',
+      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
+  END IF;
+  RETURN NULL;
+END;
+$$;
+CREATE TRIGGER for_portion_of_test_insert_trig
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_insert_trig_stmt
+AFTER INSERT ON for_portion_of_test
+REFERENCING NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_update_trig
+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();
+CREATE TRIGGER for_portion_of_test_update_trig_stmt
+AFTER UPDATE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH ROW EXECUTE PROCEDURE dump_trigger();
+CREATE TRIGGER for_portion_of_test_delete_trig_stmt
+AFTER DELETE ON for_portion_of_test
+REFERENCING OLD TABLE AS old_table
+FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger();
+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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_update_trig: UPDATE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
+NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  for_portion_of_test_delete_trig: DELETE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_delete_trig_stmt: DELETE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
+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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  for_portion_of_test_update_trig: UPDATE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+ROLLBACK;
+-- 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
+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');
+;
+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)';
+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 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-04-04)}                                                 | one^1
+ [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-15,2018-05-01)} | two
+ [3,4) | {[2018-01-01,)}                                                           | three
+(6 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
+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 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/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 34a32bd11d2..7048ea0a59e 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -1948,6 +1948,7 @@ WHERE a1.amopfamily = 0 OR a1.amoplefttype = 0 OR a1.amoprighttype = 0
 SELECT a1.amopfamily, a1.amopstrategy
 FROM pg_amop as a1
 WHERE NOT ((a1.amoppurpose = 's' AND a1.amopsortfamily = 0) OR
+           (a1.amoppurpose = 'p' AND a1.amopsortfamily = 0) OR
            (a1.amoppurpose = 'o' AND a1.amopsortfamily <> 0));
  amopfamily | amopstrategy 
 ------------+--------------
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 1d903babd33..ba8761cc9f8 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -1038,6 +1038,24 @@ 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/PERIOD 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)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) 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;
+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 8786058ed0c..53903705493 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3666,6 +3666,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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [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;
+ b | c |                        valid_at                         |  id   | two 
+---+---+---------------------------------------------------------+-------+-----
+ 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0
+ 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [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;
+ 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
+ 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0
+ 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [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 a05b64e2f78..2f857d4961f 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -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)
 
@@ -1714,6 +1778,18 @@ UPDATE temporal_rng SET id = '[7,8)'
@@ -1767,6 +1843,18 @@ UPDATE temporal_rng SET id = '[7,8)'
@@ -1803,9 +1891,22 @@ BEGIN;
@@ -1834,9 +1935,22 @@ BEGIN;
index 81e4222d26a..a4e559fe419 100644
 
 # ----------
index 00000000000..115e4bf3ec7
@@ -0,0 +1,591 @@
+SET name = 'one^1';
+FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20';
+-- With a table alias with AS
+FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t
+SET name = 'one^2';
+FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t;
+-- With a table alias without AS
+FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t
+SET name = 'one^3';
+FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t;
+-- UPDATE with FROM
+FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02'
+FROM (SELECT '[1,2)'::int4range) AS t2(id)
+WHERE for_portion_of_test.id = t2.id;
+-- DELETE with USING
+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;
+  valid1_at daterange,
+  valid2_at daterange,
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at;
+  (NULL, daterange('2018-01-01', '2019-01-01'), 'null key');
+  valid_at daterange NOT NULL,
+SET name = 'five^1'
+FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-17'))
+-- (requires a tsrange column)
+CREATE UNLOGGED TABLE for_portion_of_test2 (
+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)
+  FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE
+  TO '2012-01-01'
+-- TO is used for the bound but not the INTERVAL:
+-- syntax error
+  FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR
+  TO '2014-01-01'
+-- adding parens fixes it
+-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes)
+  FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR)
+  TO '2016-01-01'
+  RAISE NOTICE '% % % % of %', TG_WHEN, TG_OP, TG_LEVEL, NEW.valid_at, OLD.valid_at;
+CREATE TRIGGER trg_for_portion_of_before_stmt
+  FOR EACH STATEMENT
+CREATE TRIGGER trg_for_portion_of_after_stmt
+  FOR EACH STATEMENT
+    RAISE NOTICE '%: % %, NEW table = %',
+      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+    RAISE NOTICE '%: % %, OLD table = %, NEW table = %',
+      TG_NAME, TG_OP, TG_LEVEL,
+    RAISE NOTICE '%: % %, OLD table = %',
+      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
 
index 3f54b0f8f05..1d9f91567fb 100644

index ebee2f2932e..f68f78ffa35 100644
 
 --
@@ -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
@@ -1262,6 +1304,16 @@ 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)';
 -- 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')
@@ -1311,6 +1363,16 @@ ROLLBACK;
 -- 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)';
 -- 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')
@@ -1346,9 +1408,20 @@ BEGIN;

@@ -1377,9 +1450,20 @@ BEGIN;
 
 --
 -- test ON UPDATE/DELETE options
diff --git a/src/test/subscription/t/034_temporal.pl b/src/test/subscription/t/034_temporal.pl
index 3ef4249a9fe..7a6d84edf45 100644
--- a/src/test/subscription/t/034_temporal.pl
+++ b/src/test/subscription/t/034_temporal.pl
@@ -108,6 +108,12 @@ 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");
+($result, $stdout, $stderr) = $node_publisher->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)'");
+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 FOR PORTION OF temporal_no_key DEFAULT");
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
@@ -115,6 +121,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');
 
@@ -136,16 +148,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:
@@ -163,6 +181,12 @@ 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");
+($result, $stdout, $stderr) = $node_publisher->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)'");
+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 FOR PORTION OF temporal_unique DEFAULT");
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
@@ -170,6 +194,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');
 
@@ -285,16 +315,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:
@@ -308,16 +344,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:
@@ -331,17 +373,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
 
@@ -430,16 +478,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:
@@ -453,16 +507,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
@@ -573,6 +633,12 @@ 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");
+($result, $stdout, $stderr) = $node_publisher->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)'");
+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");
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_no_key WHERE id = '[3,4)'");
@@ -580,6 +646,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');
 
@@ -605,6 +677,12 @@ 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");
+($result, $stdout, $stderr) = $node_publisher->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)'");
+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");
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_pk WHERE id = '[3,4)'");
@@ -612,6 +690,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');
 
@@ -637,6 +721,12 @@ 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");
+($result, $stdout, $stderr) = $node_publisher->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)'");
+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 FOR PORTION OF temporal_unique NOTHING");
 
 ($result, $stdout, $stderr) = $node_publisher->psql('postgres',
 	"DELETE FROM temporal_unique WHERE id = '[3,4)'");
@@ -644,6 +734,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');
 
-- 
2.42.0

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

---

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 83859bac76f..cf7aa513641 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1276,7 +1276,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1291,7 +1293,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1308,7 +1313,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
index 54f3fb50a57..a21b34e2670 100644
@@ -1353,7 +1353,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 }
 
 /*
- * FindFkPeriodOpers -
@@ -1362,11 +1362,14 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
@@ -1407,6 +1410,15 @@ FindFKPeriodOpers(Oid opclass,
 
 /*
index fd2c8df56d6..02adc089858 100644
@@ -9577,6 +9577,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9672,15 +9673,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -9762,28 +9767,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
-	/*
@@ -10041,8 +10024,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
+		Oid			intersectprocoid;

 	/*
@@ -10096,6 +10083,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10106,6 +10094,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
@@ -12451,17 +12446,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12512,17 +12516,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 297fba71bb5..e4cd0ac851f 100644

 
@@ -814,7 +822,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
 	ri_PerformCheck(riinfo, &qkey, qplan,
@@ -1310,6 +1318,535 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
@@ -2280,9 +2817,10 @@ ri_LoadConstraintInfo(Oid constraintOid)

 	ReleaseSysCache(tup);
@@ -2437,8 +2975,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
 
 	/*
@@ -2473,8 +3011,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -3154,6 +3694,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 
@@ -3182,30 +3728,30 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn

index 35788315bc4..fc526c767e5 100644
 
index 3ba69424682..2165cfc20f4 100644
@@ -4036,6 +4036,28 @@
 
index def78ef8583..d669a3a89ba 100644

 explain (costs off)

 explain (costs off)
index 2f857d4961f..88ab309e135 100644
@@ -1790,6 +1790,21 @@ SET id = '[7,8)'
@@ -1855,6 +1870,21 @@ SET id = '[7,8)'
@@ -1901,12 +1931,22 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
@@ -1945,39 +1985,206 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
@@ -1985,7 +2192,432 @@ ALTER TABLE temporal_fk_rng2rng
@@ -2316,6 +2948,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
@@ -2365,6 +3013,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
@@ -2397,6 +3061,19 @@ BEGIN;
@@ -2425,6 +3102,638 @@ BEGIN;
@@ -2434,8 +3743,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -2448,8 +3757,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -2491,7 +3800,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -2503,7 +3812,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
@@ -2525,7 +3834,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -2537,37 +3846,162 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
@@ -2575,10 +4009,73 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
@@ -2698,32 +4195,150 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenc
@@ -2731,10 +4346,67 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
index f68f78ffa35..997018fd05c 100644

@@ -1314,6 +1314,8 @@ 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')
@@ -1373,6 +1375,8 @@ 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')
@@ -1416,12 +1420,11 @@ WHERE id = '[5,6)';
 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');
--- clean up:
-DELETE FROM temporal_fk_rng2rng WHERE parent_id = '[5,6)';
-DELETE FROM temporal_rng WHERE id IN ('[5,6)');
 
@@ -1458,41 +1461,131 @@ WHERE id = '[5,6)';

+--
 
+--
 
@@ -1500,6 +1593,252 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -1803,6 +2142,20 @@ COMMIT;
 
@@ -1849,6 +2202,20 @@ ROLLBACK;
 
 --
 -- test FK referenced deletes NO ACTION
@@ -1880,6 +2247,17 @@ BEGIN;

 --
 -- test FK referenced deletes RESTRICT
@@ -1909,6 +2287,417 @@ BEGIN;

@@ -1920,8 +2709,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -1935,8 +2724,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
 
 --
 -- partitioned FK referencing inserts
@@ -2029,36 +2818,90 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2066,11 +2909,34 @@ ALTER TABLE temporal_partitioned_fk_rng2rng

 
@@ -2193,36 +3059,90 @@ DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemu
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2230,11 +3150,34 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng

 
-- 
2.42.0

From 1caed66d8654e8003d7d2f853c02ceccde8c49f1 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v41 7/7] Add PERIODs

 doc/src/sgml/ref/alter_table.sgml             |   31 +-
 doc/src/sgml/ref/create_table.sgml            |   65 +-
 doc/src/sgml/trigger.sgml                     |   11 +-
 src/backend/catalog/pg_period.c               |  132 +
 src/backend/commands/tablecmds.c              |  866 ++-
 src/backend/commands/trigger.c                |    2 +
 src/backend/executor/nodeModifyTable.c        |   40 +-
 src/include/nodes/execnodes.h                 |    6 +-
 src/test/regress/expected/periods.out         |  267 +
 .../regress/expected/without_overlaps.out     | 4650 ++++++++++++++++-
 src/test/regress/sql/periods.sql              |  178 +
 src/test/regress/sql/without_overlaps.sql     | 3215 +++++++++++-
 61 files changed, 10617 insertions(+), 227 deletions(-)

index 3d2a310e821..42b217848d5 100644
 
+     <row>
@@ -5749,6 +5754,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
index 8ab0ddb112f..c0d5146256e 100644

 
 
index d8fa86dba16..43298aa29fd 100644
@@ -114,10 +116,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
-  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
-  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
 
@@ -586,6 +588,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index cf7aa513641..320591e566f 100644
@@ -73,14 +76,19 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI

 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
-  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
-  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
 class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -139,6 +147,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    name as any existing data type in the same schema.
   </para>
 
+  <para>
@@ -803,6 +819,37 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
@@ -1158,8 +1205,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1179,7 +1226,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
@@ -1187,8 +1234,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
index 6330fdff4c0..8733c17eb2b 100644
@@ -57,7 +57,8 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
   </para>
 
   <para>
index c893cc795f2..d4ce25761b8 100644
@@ -54,7 +54,8 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
   </para>
 
   <para>
index e4f63075a36..278bea99d02 100644
@@ -851,17 +851,22 @@ typedef struct ForPortionOfState
 {
     NodeTag type;
 
-    char   *fp_rangeName;       /* the column named in FOR PORTION OF */
+    char   *fp_rangeName;       /* the column/PERIOD 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 */
+    int     fp_rangeAttno;      /* the attno of the range column (or 0 for a PERIOD) */
+    int     fp_periodStartAttno;    /* the PERIOD's start column (or 0 for a range) */
+    int     fp_periodEndAttno;      /* the PERIOD's end column (or 0 for a range) */
     Datum   fp_targetRange;     /* the range from FOR PORTION OF */
 } ForPortionOfState;
 </programlisting>
 
-       where <structfield>fp_rangeName</structfield> is the range
+       where <structfield>fp_rangeName</structfield> is the period or 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,
+       <structfield>fp_periodStartAttno</structfield> and
+       <structfield>fp_periodEndAttno</structfield> are the attnos of the period's
+       start and end columns (or <symbol>0</symbol> if a range column was used),
        and <structfield>fp_targetRange</structfield> is a rangetype value created
        by evaluating the <literal>FOR PORTION OF</literal> bounds.
        </para>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 1589a75fd53..3b7af965032 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -39,6 +39,7 @@ OBJS = \
 	pg_namespace.o \
 	pg_operator.o \
 	pg_parameter_acl.o \
+	pg_period.o \
 	pg_proc.o \
 	pg_publication.o \
 	pg_range.o \
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 95eb0b12277..d2d10a083c1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2825,6 +2825,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
+					case OBJECT_PERIOD:
 					case OBJECT_PUBLICATION_NAMESPACE:
 					case OBJECT_PUBLICATION_REL:
 					case OBJECT_ROLE:
@@ -2966,6 +2967,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
 					case OBJECT_PARAMETER_ACL:
+					case OBJECT_PERIOD:
 					case OBJECT_PUBLICATION_NAMESPACE:
 					case OBJECT_PUBLICATION_REL:
 					case OBJECT_ROLE:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 0489cbabcb8..c7f26bc7189 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -48,6 +48,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_parameter_acl.h"
+#include "catalog/pg_period.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_publication.h"
@@ -622,6 +623,15 @@ findDependentObjects(const ObjectAddress *object,
 						return;
 					}
 
+					/*
 
+		case PeriodRelationId:
index 0078a12f26e..849ba1b3967 100644
 
+/*

 /*
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case PeriodRelationId:
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 05a6de68ba3..81daa268b19 100644
@@ -2174,6 +2174,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2258,6 +2259,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index 02adc089858..292628f9fe7 100644
@@ -155,6 +156,11 @@ typedef enum AlterTablePass
@@ -364,6 +370,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
@@ -446,6 +453,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
 static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode);
@@ -464,6 +473,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -664,6 +679,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -891,6 +910,79 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints);
 
+	/*
@@ -1266,6 +1358,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1363,6 +1470,312 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3011,6 +3424,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4398,12 +4973,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4412,7 +4987,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4505,6 +5080,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4833,6 +5410,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* This command never recurses */
 			pass = AT_PASS_ADD_OTHERCONSTR;
 			break;
+		case AT_AddPeriod: /* ALTER TABLE ... ADD PERIOD FOR name (start, end) */
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			pass = AT_PASS_ADD_PERIOD;
+			break;
+		case AT_DropPeriod: /* ALTER TABLE ... DROP PERIOD FOR name */
+			ATSimplePermissions(cmd->subtype, rel, ATT_TABLE);
+			pass = AT_PASS_DROP;
+			break;
 		case AT_AddIdentity:
 			ATSimplePermissions(cmd->subtype, rel,
 								ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_VIEW |
@@ -5246,6 +5831,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 		case AT_CookedColumnDefault:	/* add a pre-cooked default */
 			address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def);
 			break;
+		case AT_AddPeriod:
+			address = ATExecAddPeriod(rel, (PeriodDef *) cmd->def, context);
+			break;
+		case AT_DropPeriod:
+			ATExecDropPeriod(rel, cmd->name, cmd->behavior,
+								 false, false,
+								 cmd->missing_ok);
+			break;
 		case AT_AddIdentity:
 			cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode,
 									  cur_pass, context);
@@ -6399,6 +6992,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 		case AT_AddColumn:
 		case AT_AddColumnToView:
 			return "ADD COLUMN";
+		case AT_AddPeriod:
+			return "ADD PERIOD";
 		case AT_ColumnDefault:
 		case AT_CookedColumnDefault:
 			return "ALTER COLUMN ... SET DEFAULT";
@@ -6424,6 +7019,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "ALTER COLUMN ... SET COMPRESSION";
 		case AT_DropColumn:
 			return "DROP COLUMN";
+		case AT_DropPeriod:
+			return "DROP PERIOD";
 		case AT_AddIndex:
 		case AT_ReAddIndex:
 			return NULL;		/* not real grammar */
@@ -7409,14 +8006,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 /*
  * If a new or renamed column will collide with the name of an existing
  * column and if_not_exists is false then error out, else do nothing.
+ *
+ * See also check_for_period_name_collision.
  */
 static bool
 check_for_column_name_collision(Relation rel, const char *colname,
 								bool if_not_exists)
 {
-	HeapTuple	attTuple;
+	HeapTuple	attTuple, perTuple;
 	int			attnum;
 
@@ -7460,6 +8072,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -7969,6 +8652,157 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -9745,8 +10579,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -13547,6 +14382,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 
 				/*
@@ -13636,6 +14481,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*

diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 55a25a36c8b..94d9ecdcba2 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -6058,6 +6058,8 @@ CopyForPortionOfState(ForPortionOfState *src)
 		dst->fp_rangeName = pstrdup(src->fp_rangeName);
 		dst->fp_rangeType = src->fp_rangeType;
 		dst->fp_rangeAttno = src->fp_rangeAttno;
+		dst->fp_periodStartAttno = src->fp_periodStartAttno;
+		dst->fp_periodEndAttno = src->fp_periodEndAttno;
 
 		r = DatumGetRangeTypeP(src->fp_targetRange);
 		typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index fdad8338324..3ae225d6798 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -164,7 +164,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			}

 
 
 		/*
 		 * There is very little to do here to update the view's dependencies.
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 9f2234320f6..9c14381b52b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1370,6 +1370,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	FmgrInfo flinfo;
 	ReturnSetInfo rsi;
 	Relation rel;
+	bool hasPeriod = false;
 	bool didInit = false;
 	bool shouldFree = false;
 	LOCAL_FCINFO(fcinfo, 2);
@@ -1479,6 +1480,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 			else
 				leftoverSlot = oldtupleSlot;
 
+			hasPeriod = forPortionOf->startVar;
 			tupdesc = leftoverSlot->tts_tupleDescriptor;
 			natts = tupdesc->natts;
 
@@ -1579,8 +1581,42 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 				fpoState->fp_nulls[i] = leftoverSlot->tts_isnull[i] ? 'n' : ' ';
 		}
 
-		fpoState->fp_nulls[forPortionOf->rangeVar->varattno - 1] = ' ';
-		fpoState->fp_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+		if (hasPeriod)
+		{
+			RangeType  *leftoverRange;
+			RangeBound	leftoverLower;
+			RangeBound	leftoverUpper;
+			bool		leftoverEmpty;
+			AttrNumber	startAttno;
+			AttrNumber	endAttno;
+
+			leftoverRange = DatumGetRangeTypeP(leftover);
+			range_deserialize(typcache, leftoverRange, &leftoverLower, &leftoverUpper, &leftoverEmpty);
+
+			startAttno = forPortionOf->startVar->varattno;
+			endAttno = forPortionOf->endVar->varattno;
+
+			if (leftoverLower.infinite)
+				fpoState->fp_nulls[startAttno - 1] = 'n';
+			else
+			{
+				fpoState->fp_nulls[startAttno - 1] = ' ';
+				fpoState->fp_values[startAttno - 1] = leftoverLower.val;
+			}
+
+			if (leftoverUpper.infinite)
+				fpoState->fp_nulls[endAttno - 1] = 'n';
+			else
+			{
+				fpoState->fp_nulls[endAttno - 1] = ' ';
+				fpoState->fp_values[endAttno - 1] = leftoverUpper.val;
+			}
+		}
+		else
+		{
+			fpoState->fp_nulls[forPortionOf->rangeVar->varattno - 1] = ' ';
+			fpoState->fp_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+		}
 
 		spi_result = SPI_execute_snapshot(qplan,
 										  fpoState->fp_values,
index 95011120495..c47ca62cb3c 100644
index 8610ab403bd..be5aef4857b 100644
 
+	/*
 
@@ -1411,12 +1466,44 @@ transformForPortionOfClause(ParseState *pstate,

index eced82efd23..b7df7337832 100644
 
@@ -2661,6 +2661,24 @@ alter_table_cmd:
@@ -3791,8 +3809,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4141,6 +4161,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7236,6 +7269,14 @@ CommentStmt:
@@ -17839,7 +17880,6 @@ unreserved_keyword:
@@ -18143,6 +18183,7 @@ reserved_keyword:
index 8075b1b8a1b..6ead81450b8 100644
@@ -3262,6 +3263,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
@@ -3285,12 +3287,20 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,

index 1e15ce10b48..d068096fda5 100644
@@ -109,6 +111,8 @@ typedef struct
 
 static void transformColumnDefinition(CreateStmtContext *cxt,
 									  ColumnDef *column);
+static void transformTablePeriod(CreateStmtContext *cxt,
+								 PeriodDef *period);
 static void transformTableConstraint(CreateStmtContext *cxt,
 									 Constraint *constraint);
 static void transformTableLikeClause(CreateStmtContext *cxt,
@@ -239,6 +243,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 	cxt.inhRelations = stmt->inhRelations;
 	cxt.isalter = false;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
@@ -277,6 +282,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 				transformColumnDefinition(&cxt, (ColumnDef *) element);
 				break;
 
+			case T_PeriodDef:
@@ -339,6 +348,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
 	result = lappend(cxt.blist, stmt);
@@ -895,6 +905,91 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
@@ -982,6 +1077,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1033,6 +1129,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1042,10 +1139,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
@@ -2432,6 +2537,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
@@ -2450,19 +2556,40 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			if (!found)
 				column = NULL;
 
@@ -2586,7 +2713,12 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
@@ -2937,6 +3069,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 		}
 	}
 
+	/* take care of the period */
+	if (stmt->period)
+		stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false);
+
 	/*
 	 * Check that only the base rel is mentioned.  (This should be dead code
 	 * now that add_missing_from is history.)
@@ -3394,6 +3530,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 	cxt.inhRelations = NIL;
 	cxt.isalter = true;
 	cxt.columns = NIL;
+	cxt.periods = NIL;
 	cxt.ckconstraints = NIL;
 	cxt.fkconstraints = NIL;
 	cxt.ixconstraints = NIL;
@@ -3454,6 +3591,22 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
+					/*
index 85e5eaf32eb..9a5ef1a11a8 100644
 	pg_locale_icu.o \
 	pg_locale_libc.o \
index 254731830ba..079bf953de2 100644

 /*
 
+Oid
 
 /*
index 1b47c388ced..bc3e60f31d9 100644
@@ -6785,6 +6785,7 @@ getTables(Archive *fout, int *numTables)
@@ -6862,6 +6863,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6999,6 +7008,7 @@ getTables(Archive *fout, int *numTables)
@@ -7082,6 +7092,7 @@ getTables(Archive *fout, int *numTables)
@@ -8715,7 +8726,7 @@ getTransforms(Archive *fout)
@@ -8765,6 +8776,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8779,7 +8792,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9141,15 +9155,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9171,6 +9206,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9190,12 +9226,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9254,6 +9291,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10482,6 +10593,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16040,6 +16153,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16048,7 +16188,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16294,7 +16434,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 */
 			resetPQExpBuffer(extra);
 			firstitem = true;
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -16600,7 +16740,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18673,6 +18813,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
index 9f907ed5ad4..3c7c42f2a4c 100644
@@ -301,12 +302,14 @@ typedef struct _tableInfo
@@ -337,6 +340,7 @@ typedef struct _tableInfo
@@ -351,6 +355,7 @@ typedef struct _tableInfo
 
@@ -480,6 +485,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
index 363a66e7185..1daa44c243e 100644

 
+		/* print periods */
index d6a2c791290..b55fda7b696 100644
 
index 6c89639a9e4..0deb6d4cc8f 100644

 
 /*

 

 #endif							/* TABLECMDS_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 8da5618ee19..8073bf700b5 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -442,9 +442,11 @@ typedef struct ForPortionOfState
 {
 	NodeTag		type;
 
-	char   *fp_rangeName;		/* the column named in FOR PORTION OF */
+	char   *fp_rangeName;		/* the column/PERIOD 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 */
+	int		fp_rangeAttno;		/* the attno of the range column (or 0 for a PERIOD) */
+	int		fp_periodStartAttno;	/* the attno of the PERIOD start column (or 0 for a range) */
+	int		fp_periodEndAttno;		/* the attno of the PERIOD end column (or 0 for a range) */
 	Datum	fp_targetRange;		/* the range from FOR PORTION OF */
 	TypeCacheEntry *fp_leftoverstypcache;	/* type cache entry of the range */
 	TupleTableSlot *fp_Existing;		/* slot to store old tuple */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e86c3a5a0ed..9fa570971a8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2303,6 +2303,7 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_PARAMETER_ACL,
+	OBJECT_PERIOD,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
@@ -2391,6 +2392,8 @@ typedef enum AlterTableType
 	AT_ValidateConstraint,		/* validate constraint */
 	AT_AddIndexConstraint,		/* add constraint using existing index */
 	AT_DropConstraint,			/* drop constraint */
+	AT_AddPeriod,				/* ADD PERIOD */
+	AT_DropPeriod,				/* DROP PERIOD */
 	AT_ReAddComment,			/* internal to commands/tablecmds.c */
 	AT_AlterColumnType,			/* alter column type */
 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
@@ -2672,11 +2675,11 @@ typedef struct VariableShowStmt
 /* ----------------------
  *		Create Table Statement
  *
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints is NIL.  After parse analysis,
- * tableElts contains just ColumnDefs, and constraints contains just
- * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present
- * implementation).
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; periods and constraints are NIL.  After parse analysis,
+ * tableElts contains just ColumnDefs, periods contains just PeriodDefs, and
+ * constraints contains just Constraint nodes (in fact, only CONSTR_CHECK nodes,
+ * in the present implementation).
  * ----------------------
  */
 
@@ -2685,6 +2688,7 @@ typedef struct CreateStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to create */
 	List	   *tableElts;		/* column definitions (list of ColumnDef) */
+	List	   *periods;		/* periods (list of PeriodDef nodes) */
 	List	   *inhRelations;	/* relations to inherit from (list of
 								 * RangeVar) */
 	PartitionBoundSpec *partbound;	/* FOR VALUES clause */
@@ -2698,6 +2702,31 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3391,6 +3420,7 @@ typedef struct IndexStmt
index 8392018a821..bbfd185530d 100644
@@ -2354,6 +2354,8 @@ typedef struct ForPortionOfExpr
index 425987afee7..46457d60087 100644
@@ -338,7 +338,7 @@ PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 
index 2758ae82d7b..d90b484f34d 100644
index db217c61d2f..d66dc73a0f1 100644
@@ -599,6 +599,44 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
  [3,4) | {[2018-01-01,)}                                                           | three
 (6 rows)
 
+ [1,2) | 2018-01-02 | 2018-01-10 | one
+ [1,2) | 2018-01-10 | 2018-02-03 | one^1
+ [1,2) | 2018-02-04 | 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
index 88ab309e135..873b8f2f41a 100644

 
@@ -1405,32 +1925,210 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
@@ -1458,6 +2156,18 @@ CREATE TABLE temporal_fk_rng2rng (
@@ -3706,99 +4416,3829 @@ SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, val
 
-  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;
+DROP TABLE temporal_fk_per2per;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_per2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_per2;
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2per
+           Table "public.temporal_fk2_per2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2per_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_per2per;
 --
--- FK between partitioned tables: ranges
+-- test ALTER TABLE ADD CONSTRAINT
 --
-CREATE TABLE temporal_partitioned_rng (
-  id int4range,
-  valid_at daterange,
-  name text,
-  CONSTRAINT temporal_paritioned_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)', '[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'),
-  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
-CREATE TABLE temporal_partitioned_fk_rng2rng (
+CREATE TABLE temporal_fk_per2per (
   id int4range,
-  valid_at daterange,
+  valid_from date,
+  valid_til date,
   parent_id int4range,
-  CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
-  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)', '[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)');
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2per
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2per
+           Table "public.temporal_fk2_per2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2per_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ERROR:  foreign key constraint "temporal_fk_per2per_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsrange and daterange.
+ALTER TABLE temporal_fk_per2per
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
 --
--- partitioned FK referencing inserts
+-- test with rows already
 --
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
-  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
-  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
-  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
+DELETE FROM temporal_fk_per2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
 -- should fail:
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
-  ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
-ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
-DETAIL:  Key (parent_id, valid_at)=([1,2), [2010-01-01,2010-02-15)) is not present in table "temporal_partitioned_rng".
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
-  ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
-ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
-DETAIL:  Key (parent_id, valid_at)=([3,4), [2000-01-01,2000-02-15)) is not present in table "temporal_partitioned_rng".
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- okay again:
+DELETE FROM temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
 --
--- partitioned FK referencing updates
+-- test pg_get_constraintdef
 --
-UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,3)';
--- move a row from the first partition to the second
-UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,5)' WHERE id = '[1,2)';
--- move a row from the second partition to the first
-UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,2)' WHERE id = '[4,5)';
--- should fail:
-UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,2)';
-ERROR:  conflicting key value violates exclusion constraint "tfkp1_pkey"
-DETAIL:  Key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)) conflicts with existing key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)).
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2per_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_per(id, PERIOD valid_at)
+(1 row)
+
 --
--- partitioned FK referenced updates NO ACTION
+-- test FK referencing inserts
 --
-TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
-INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
-UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
-INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
-UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
-  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
 -- should fail:
-UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-01-01', '2016-02-01')
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+--
+-- test FK referencing updates
+--
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-05-01)) is not present in table "temporal_per".
+UPDATE temporal_fk_per2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2per" violates foreign key constraint "temporal_fk_per2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-02-20)) is not present in table "temporal_per".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+--
+-- test FK referenced updates NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK referenced updates RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK referenced deletes NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK referenced deletes RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_per2per_fk" on table "temporal_fk_per2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- per2per 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id 
+----+------------+-----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | parent_id 
+-----------+------------+------------+-----------
+ [200,201) | 2020-01-01 | 2021-01-01 | [8,9)
+(1 row)
+
+--
+-- test FK referenced updates SET NULL
+--
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id1 | parent_id2 
+----+------------+-----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    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_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    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_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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, range references PERIOD
+--
+-- test table setup
+DROP TABLE temporal_per CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk_per2per_fk on table temporal_fk_per2per
+CREATE TABLE temporal_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal_per
+  ADD CONSTRAINT temporal_per_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at int4range,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_rng2per_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4range and daterange.
+-- works: PERIOD for referenced, range for referencing
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_rng2per
+           Table "public.temporal_fk_rng2per"
+  Column   |   Type    | Collation | Nullable | Default 
+-----------+-----------+-----------+----------+---------
+ id        | int4range |           | not null | 
+ valid_at  | daterange |           | not null | 
+ parent_id | int4range |           |          | 
+Indexes:
+    "temporal_fk_rng2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk_rng2per_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_per(id, PERIOD valid_at)
+
+DROP TABLE temporal_fk_rng2per;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_rng2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_per2 CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk2_per2per_fk on table temporal_fk2_per2per
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_rng2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2per
+           Table "public.temporal_fk2_rng2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | daterange |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_rng2per;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2per
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2per
+           Table "public.temporal_fk2_rng2per"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_at   | daterange |           | not null | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Indexes:
+    "temporal_fk2_rng2per_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Foreign-key constraints:
+    "temporal_fk2_rng2per_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_per2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ERROR:  foreign key constraint "temporal_fk_rng2per_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsrange and daterange.
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_rng2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,1]', '[2018-01-02,2018-02-01)', '[1,1]');
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ERROR:  insert or update on table "temporal_fk_rng2per" violates foreign key constraint "temporal_fk_rng2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- okay again:
+DELETE FROM temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2per_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_per(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK referencing inserts
+--
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,1]', '[2018-01-02,2018-02-01)', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+ERROR:  insert or update on table "temporal_fk_rng2per" violates foreign key constraint "temporal_fk_rng2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_per".
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+--
+-- test FK referencing updates
+--
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-02-20)' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-05-01)' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_rng2per" violates foreign key constraint "temporal_fk_rng2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-05-01)) is not present in table "temporal_per".
+UPDATE temporal_fk_rng2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_rng2per" violates foreign key constraint "temporal_fk_rng2per_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-02-20)) is not present in table "temporal_per".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+    ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+--
+-- test FK referenced updates NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[4,5)', '[2018-01-15,2018-02-15)', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_rng2per WHERE id in ('[3,3]') 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_rng2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+--
+-- test FK referenced updates RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[4,5)', '[2018-01-15,2018-02-15)', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [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_rng2per WHERE id in ('[3,3]') 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_rng2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK referenced deletes NO ACTION
+--
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') 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_rng2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- test FK referenced deletes RESTRICT
+--
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_per" violates foreign key constraint "temporal_fk_rng2per_fk" on table "temporal_fk_rng2per"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2per".
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  
+-------+------------+------------
+ [5,6) | 2018-01-01 | 2018-01-02
+ [5,6) | 2018-01-03 | 2018-02-01
+(2 rows)
+
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') 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_rng2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+--
+-- rng2per 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' 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_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_at | parent_id1 | parent_id2 
+----+----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    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_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    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_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' 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_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' 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, PERIOD references range
+--
+-- test table setup
+DROP TABLE temporal_rng CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+ALTER TABLE temporal_rng
+  ADD CONSTRAINT temporal_rng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from int,
+  valid_til int,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key constraint "temporal_fk_per2rng_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: int4range and daterange.
+-- works: range for referenced, PERIOD for referencing
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+\d temporal_fk_per2rng
+           Table "public.temporal_fk_per2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id  | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk_per2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk_per2rng_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk_per2rng_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+
+DROP TABLE temporal_fk_per2rng;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+ERROR:  foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id)
+);
+ERROR:  foreign key uses PERIOD on the referencing table but not the referenced table
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_per2rng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng
+);
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_rng (id, PERIOD id)
+);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+-- Two scalar columns
+DROP TABLE temporal_rng2 CASCADE;
+NOTICE:  drop cascades to constraint temporal_fk2_rng2rng_fk on table temporal_fk2_rng2rng
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2rng
+           Table "public.temporal_fk2_per2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2rng_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+DROP TABLE temporal_fk2_per2rng;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2rng
+  ADD CONSTRAINT temporal_fk2_per2rng_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2rng
+           Table "public.temporal_fk2_per2rng"
+   Column   |   Type    | Collation | Nullable | Default 
+------------+-----------+-----------+----------+---------
+ id         | int4range |           | not null | 
+ valid_from | date      |           |          | 
+ valid_til  | date      |           |          | 
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+Periods:
+    valid_at (valid_from, valid_til)
+Indexes:
+    "temporal_fk2_per2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+Check constraints:
+    "temporal_fk2_per2rng_valid_at_check" CHECK (valid_from < valid_til)
+Foreign-key constraints:
+    "temporal_fk2_per2rng_fk" FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at) REFERENCES temporal_rng2(id1, id2, PERIOD valid_at)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ERROR:  foreign key constraint "temporal_fk_per2rng_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsrange and daterange.
+ALTER TABLE temporal_fk_per2rng
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_rng (id, PERIOD id);
+ERROR:  foreign key referenced-columns list must not contain duplicates
+--
+-- test with rows already
+--
+DELETE FROM temporal_fk_per2rng;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,1]', '[2018-01-02,2018-02-03)'),
+  ('[1,1]', '[2018-03-03,2018-04-04)'),
+  ('[2,2]', '[2018-01-01,2018-01-05)'),
+  ('[3,3]', '[2018-01-01,)');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ERROR:  insert or update on table "temporal_fk_per2rng" violates foreign key constraint "temporal_fk_per2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_rng".
+-- okay again:
+DELETE FROM temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2rng_fk';
+                                 pg_get_constraintdef                                  
+---------------------------------------------------------------------------------------
+ FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
+(1 row)
+
+--
+-- test FK referencing inserts
+--
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+ERROR:  insert or update on table "temporal_fk_per2rng" violates foreign key constraint "temporal_fk_per2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-04-01)) is not present in table "temporal_rng".
+-- now it should work:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,1]', '[2018-02-03,2018-03-03)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+--
+-- test FK referencing updates
+--
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2rng" violates foreign key constraint "temporal_fk_per2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2018-01-02,2018-05-01)) is not present in table "temporal_rng".
+UPDATE temporal_fk_per2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+ERROR:  insert or update on table "temporal_fk_per2rng" violates foreign key constraint "temporal_fk_per2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([8,9), [2018-01-02,2018-02-20)) is not present in table "temporal_rng".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES
+    ('[5,5]', '[2018-01-01,2018-02-01)'),
+    ('[5,5]', '[2018-02-01,2018-03-01)');
+  INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+--
+-- test FK referenced updates NO ACTION
+--
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_rng SET valid_at = '[2016-02-01,2016-03-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[6,7)', '[2018-01-01,2018-02-01)'),
+  ('[6,7)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_rng
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') 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_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+--
+-- test FK referenced updates RESTRICT
+--
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_rng SET valid_at = '[2016-02-01,2016-03-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[6,7)', '[2018-01-01,2018-02-01)'),
+  ('[6,7)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_rng
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+WHERE id = '[6,7)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+  WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') 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_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+--
+-- test FK referenced deletes NO ACTION
+--
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+-- 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,5]';
+-- 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,5]';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') 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_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+--
+-- test FK referenced deletes RESTRICT
+--
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON DELETE RESTRICT;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+ROLLBACK;
+-- 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,5]';
+-- 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,5]';
+ERROR:  update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_per2rng_fk" on table "temporal_fk_per2rng"
+DETAIL:  Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_per2rng".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') 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_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+  id   | valid_from | valid_til  | 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_per2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+--
+-- per2rng 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+    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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id 
+----+------------+-----------+-----------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+ id | valid_from | valid_til | parent_id1 | parent_id2 
+----+------------+-----------+------------+------------
+(0 rows)
+
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,6]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,8]', '(,)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+    id     | valid_from | valid_til  | 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 (
+  id int4range,
+  valid_at daterange,
+  name text,
+  CONSTRAINT temporal_paritioned_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)', '[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'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  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)', '[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
+--
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
+  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
+  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([1,2), [2010-01-01,2010-02-15)) is not present in table "temporal_partitioned_rng".
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
+ERROR:  insert or update on table "tfkp1" violates foreign key constraint "temporal_partitioned_fk_rng2rng_fk"
+DETAIL:  Key (parent_id, valid_at)=([3,4), [2000-01-01,2000-02-15)) is not present in table "temporal_partitioned_rng".
+--
+-- partitioned FK referencing updates
+--
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-02-13') WHERE id = '[2,3)';
+-- move a row from the first partition to the second
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[4,5)' WHERE id = '[1,2)';
+-- move a row from the second partition to the first
+UPDATE temporal_partitioned_fk_rng2rng SET id = '[1,2)' WHERE id = '[4,5)';
+-- should fail:
+UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,2)';
+ERROR:  conflicting key value violates exclusion constraint "tfkp1_pkey"
+DETAIL:  Key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)) conflicts with existing key (id, valid_at)=([1,2), [2000-01-01,2000-04-01)).
+--
+-- partitioned FK referenced updates NO ACTION
+--
+TRUNCATE temporal_partitioned_rng, temporal_partitioned_fk_rng2rng;
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2016-01-01', '2016-02-01'));
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2018-01-01', '2018-02-01') WHERE id = '[5,6)';
+INSERT INTO temporal_partitioned_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-02-01', '2018-03-01'));
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
+-- 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_parent_id_valid_at_fkey1" 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".
@@ -4409,4 +8849,18 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 
index a4e559fe419..2cac8aadf4d 100644
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index 115e4bf3ec7..6e9571e879f 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -481,6 +481,38 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;

+-- Test with PERIODs
+
 
index 997018fd05c..009a85d04cd 100644

 
 
+--
@@ -962,6 +1272,121 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;
@@ -1015,6 +1440,7 @@ CREATE TABLE temporal_fk_rng2rng (
 
 -- with mismatched PERIOD columns:
@@ -2673,70 +3099,2740 @@ INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VA
 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;

 
 
---
 
-CREATE TABLE temporal_partitioned_rng (
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
   id int4range,
-  valid_at daterange,
-  name text,
-  CONSTRAINT temporal_paritioned_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)', '[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'),
-  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
-
-CREATE TABLE temporal_partitioned_fk_rng2rng (
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2per (
   id int4range,
-  valid_at daterange,
+  valid_from date,
+  valid_til date,
   parent_id int4range,
-  CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
-  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)', '[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
---
-
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
-  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
-  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
-  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
--- should fail:
-INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_per2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_per2;
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2per
+DROP TABLE temporal_fk2_per2per;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2per
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2per
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_per2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- okay again:
+DELETE FROM temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2per_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+UPDATE temporal_fk_per2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- per2per 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FOREIGN KEY, range references PERIOD
+--
+
+-- test table setup
+DROP TABLE temporal_per CASCADE;
+CREATE TABLE temporal_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal_per
+  ADD CONSTRAINT temporal_per_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at int4range,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+
+-- works: PERIOD for referenced, range for referencing
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_rng2per
+DROP TABLE temporal_fk_rng2per;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_rng2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_per2 CASCADE;
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_rng2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2per
+DROP TABLE temporal_fk2_rng2per;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2per
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2per
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,1]', '2018-01-02', '2018-02-03'),
+  ('[1,1]', '2018-03-03', '2018-04-04'),
+  ('[2,2]', '2018-01-01', '2018-01-05'),
+  ('[3,3]', '2018-01-01', NULL);
+
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,1]', '[2018-01-02,2018-02-01)', '[1,1]');
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- okay again:
+DELETE FROM temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2per_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,1]', '[2018-01-02,2018-02-01)', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,1]', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,2]', '[2018-01-02,2018-04-01)', '[1,1]');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-02-20)' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-05-01)' WHERE id = '[1,1]';
+UPDATE temporal_fk_rng2per SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,5]', '2018-01-01', '2018-02-01'),
+    ('[5,5]', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+    ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[4,5)', '[2018-01-15,2018-02-15)', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,5]';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[4,5)', '[2018-01-15,2018-02-15)', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,3]';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,5]', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,5]', '2018-01-01', '2018-02-01'),
+  ('[5,5]', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,3]', '[2018-01-05,2018-01-10)', '[5,5]');
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ROLLBACK;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,5]';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_per WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,3]';
+DELETE FROM temporal_per WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- rng2per 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+UPDATE temporal_per SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]');
+DELETE FROM temporal_per WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+UPDATE temporal_per2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,6]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,6]', '[6,6]', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,8]', '[8,8]', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,8]', null, null);
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,8]', '[8,8]');
+DELETE FROM temporal_per2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FOREIGN KEY, PERIOD references range
+--
+
+-- test table setup
+DROP TABLE temporal_rng CASCADE;
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+ALTER TABLE temporal_rng
+  ADD CONSTRAINT temporal_rng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from int,
+  valid_til int,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+-- works: range for referenced, PERIOD for referencing
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+\d temporal_fk_per2rng
+DROP TABLE temporal_fk_per2rng;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_per2rng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_rng (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_rng2 CASCADE;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2rng
+DROP TABLE temporal_fk2_per2rng;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2rng
+  ADD CONSTRAINT temporal_fk2_per2rng_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2rng
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_per2rng;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,1]', '[2018-01-02,2018-02-03)'),
+  ('[1,1]', '[2018-03-03,2018-04-04)'),
+  ('[2,2]', '[2018-01-01,2018-01-05)'),
+  ('[3,3]', '[2018-01-01,)');
+
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2rng_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+-- now it should work:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,1]', '[2018-02-03,2018-03-03)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]';
+UPDATE temporal_fk_per2rng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES
+    ('[5,5]', '[2018-01-01,2018-02-01)'),
+    ('[5,5]', '[2018-02-01,2018-03-01)');
+  INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,5]'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_rng SET valid_at = '[2016-02-01,2016-03-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[6,7)', '[2018-01-01,2018-02-01)'),
+  ('[6,7)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_rng
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = '[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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+UPDATE temporal_rng SET valid_at = '[2016-02-01,2016-03-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[6,7)', '[2018-01-01,2018-02-01)'),
+  ('[6,7)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_rng
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+WHERE id = '[6,7)';
+-- a PK update that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+  WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ROLLBACK;
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = '[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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_rng
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,7]'
+WHERE id = '[5,5]';
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK update succeeds:
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,3]';
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  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,5]';
+-- 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,5]';
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON DELETE RESTRICT;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,5]', '[2018-01-01,2018-02-01)');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,5]', '[2018-01-01,2018-02-01)'),
+  ('[5,5]', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]');
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-02-01,2018-03-01)';
+-- a PK delete that fails because both are referenced (even before commit):
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+ROLLBACK;
+-- 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,5]';
+-- 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,5]';
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,3]';
+DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- per2rng 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+    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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+ALTER TABLE temporal_fk_per2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+UPDATE temporal_rng SET id = '[9,9]' WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]');
+DELETE FROM temporal_rng WHERE id = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,7]', id2 = '[7,7]' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+UPDATE temporal_rng2 SET id1 = '[9,9]', id2 = '[9,9]' WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- leftovers on both sides:
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,6]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,6]', '[6,6]', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,6]', '[6,6]');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,6]';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,8]', '[8,8]', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,8]', '(,)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,8]', '[8,8]');
+DELETE FROM temporal_rng2 WHERE id1 = '[8,8]' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[200,200]' 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
+--
+
+CREATE TABLE temporal_partitioned_rng (
+  id int4range,
+  valid_at daterange,
+  name text,
+  CONSTRAINT temporal_paritioned_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)', '[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'),
+  ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
+
+CREATE TABLE temporal_partitioned_fk_rng2rng (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_partitioned_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  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)', '[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
+--
+
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2000-01-01', '2000-02-15'), '[1,2)'),
+  ('[1,2)', daterange('2001-01-01', '2002-01-01'), '[2,3)'),
+  ('[2,3)', daterange('2000-01-01', '2000-02-15'), '[1,2)');
+-- should fail:
+INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[3,4)', daterange('2010-01-01', '2010-02-15'), '[1,2)');
 INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[3,4)', daterange('2000-01-01', '2000-02-15'), '[3,4)');
@@ -3181,4 +6277,19 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
+--

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 24 Oct 2024 17:22:32 -0700

> FOR PORTION OF etc patches. I've also added the logical replication problem to the v18 Open Items 

New patches attached to fix some conflicts. I don't think there is anything else except cleaning up 
some sloppy white space.

Rebased to d32d146399.

Yours,

From af829e16ef4807d10b21da4ef5657b10597c419f Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:02:12 -0700
Subject: [PATCH v42 1/7] Add WITHOUT OVERLAPS and PERIOD to ALTER TABLE
 reference docs

 
 
-- 
2.42.0

From a7c126c12676acf5342a908f3e9b26f52ada32ba Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:12:46 -0700
Subject: [PATCH v42 2/7] Update conexclop docs for WITHOUT OVERLAPS

---

-- 
2.42.0

From b2be8e767b62542e774968420fef6457714f2762 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 16 Oct 2024 11:06:40 -0700
Subject: [PATCH v42 3/7] Fix logical replication for temporal tables

index e14bc0c0548..93361249bdb 100644
@@ -16884,9 +16884,14 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode

 
 /*

 

From 7b0e1c595c12781dadba36a988bd15171a778767 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v42 4/7] Add support funcs for FOR PORTION OF

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>

 	va_start(ap, maxargs);
 
 /*
 
+/*

 
 Datum

 /*
 
 # gin
index 1ec0d6f6b5f..ba8528d64d3 100644
@@ -10737,6 +10737,10 @@
@@ -11024,6 +11028,10 @@

+-- without_portion

+-- without_portion

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

 src/include/nodes/parsenodes.h                |  20 +
 52 files changed, 3205 insertions(+), 102 deletions(-)

 

 
+  <para>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>
 
+   <para>

 

index 93361249bdb..6060a9080a2 100644
@@ -12422,6 +12422,7 @@ validateForeignKeyConstraint(char *conname,

 

 

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

 #include "postgres.h"

 
+/*
+ * FPO_QueryHashEntry
+ */

 /*

 

+	/*

 
 
index 506e0631615..8d6876a41f9 100644
 
 #include "postgres.h"

@@ -59,10 +64,16 @@
 
@@ -567,6 +578,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -600,6 +625,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -638,7 +664,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -1274,7 +1304,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1304,6 +1334,186 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2512,6 +2722,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2529,6 +2740,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2545,7 +2760,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2555,7 +2771,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2574,7 +2790,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2627,6 +2843,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index baca4059d2e..9a4a9761e98 100644
@@ -244,6 +244,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -540,6 +541,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -749,7 +752,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER

@@ -868,12 +871,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -12309,6 +12315,20 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 					n->stmt_location = @$;
 
 using_clause:
@@ -12384,6 +12404,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->stmt_location = @$;
 					$$ = (Node *) n;
 				}
 
 set_clause_list:
@@ -13867,6 +13906,44 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+opt_alias:
+			AS ColId
+				{
@@ -14700,16 +14777,25 @@ opt_timezone:
 
+/*
-			| MINUTE_P
+			| MINUTE_P														%prec IS
 				{ $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); }
@@ -17760,6 +17846,7 @@ unreserved_keyword:
@@ -18385,6 +18472,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 /*

 

 

 	/*
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *

 /*
 
+/*
+ * get_typname
+ *
 
 /*

+/*
+ * ForPortionOfState
+ *

index b40b661ec8a..eabadd47304 100644

+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <period-name> FROM <ts> TO <te>
+ *		or FOR PORTION OF <period-name> (<target>)
+ */
+typedef struct ForPortionOfClause
+{
+	NodeTag		type;
+	char	   *range_name;
+	int			location;
+	Node	   *target;
+	Node	   *target_start;
+	Node	   *target_end;
+} ForPortionOfClause;
+
 /*
  * WithClause -
  *	   representation of WITH clause
@@ -2064,6 +2082,7 @@ typedef struct DeleteStmt
 	WithClause *withClause;		/* WITH clause */
 	ParseLoc	stmt_location;	/* start location, or -1 if unknown */
 	ParseLoc	stmt_len;		/* length in bytes; 0 means "rest of string" */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
 } DeleteStmt;
 
 /* ----------------------
@@ -2081,6 +2100,7 @@ typedef struct UpdateStmt
 	WithClause *withClause;		/* WITH clause */
 	ParseLoc	stmt_location;	/* start location, or -1 if unknown */
 	ParseLoc	stmt_len;		/* length in bytes; 0 means "rest of string" */
+	ForPortionOfClause *forPortionOf;	/* FOR PORTION OF clause */
 } UpdateStmt;
 
index add0f9e45fc..24497ca1d8c 100644
@@ -2395,6 +2395,7 @@ typedef struct ModifyTablePath
 
+/*----------
index 2375e95c107..85a4a909e7f 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 

 # ----------

 

 -- temporal UNIQUE:

 

 

 

 

 

 

 

 

 

 
 # cleanup

 

 

 

 # cleanup

 

 

 
-- 
2.42.0

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

---

          <para>
 
          <para>
 
          <para>
 
 /*
- * FindFkPeriodOpers -
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6060a9080a2..4aea103f95c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -516,7 +516,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
 											   Relation rel, Constraint *fkconstraint,
 											   bool recurse, bool recursing,
 											   LOCKMODE lockmode);
-static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, const int16 fkperiodattnum,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
 static ObjectAddress addFkConstraint(addFkConstraintSides fkside,
@@ -9594,6 +9594,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,
@@ -9689,15 +9690,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
 	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];
+	}

@@ -9779,28 +9784,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
-	/*
@@ -10058,8 +10041,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
+		Oid			intersectprocoid;

 	/* First, create the constraint catalog entry itself. */
@@ -10126,6 +10113,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10136,6 +10124,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
@@ -12550,17 +12545,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12611,17 +12615,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

 

+/*

 	ReleaseSysCache(tup);
 
 	/*

 
 
index ba8528d64d3..0d93c1b6941 100644

 
 explain (costs off)

 explain (costs off)

 

+--
 
+--

 

 

 

 

 

 

 

 

 

-- 
2.42.0

From c0837dde235defaebd8f605395b308e361fc6374 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v42 7/7] Add PERIODs

 
+     <row>
index f6344b3b79a..fbfef21df7c 100644

 

 

 
 
+  <para>

 
      <para>
 
   <para>
 
   <para>

 
+					/*
 
+		case PeriodRelationId:
 
+/*

 /*
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case PeriodRelationId:
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 4aea103f95c..730c944a2eb 100644
@@ -372,6 +378,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
@@ -454,6 +461,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
@@ -472,6 +481,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -681,6 +696,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -908,6 +927,79 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints);
 
+	/*
@@ -1283,6 +1375,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1380,6 +1487,312 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3028,6 +3441,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4415,12 +4990,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4429,7 +5004,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4522,6 +5097,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4850,6 +5427,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5263,6 +5848,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6416,6 +7009,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6441,6 +7036,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7426,14 +8023,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7477,6 +8089,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -7986,6 +8669,157 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -9762,8 +10596,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -13646,6 +14481,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 
 				/*
@@ -13735,6 +14580,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -15629,7 +16483,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 
 		/*

 
 
index 8d6876a41f9..ec493d53ea0 100644
@@ -1356,7 +1357,11 @@ transformForPortionOfClause(ParseState *pstate,
@@ -1400,6 +1405,53 @@ transformForPortionOfClause(ParseState *pstate,
 
+	/*
 
@@ -1467,7 +1519,10 @@ transformForPortionOfClause(ParseState *pstate,
@@ -1496,12 +1551,44 @@ transformForPortionOfClause(ParseState *pstate,

 		/* Mark the range column as requiring update permissions */
 		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9a4a9761e98..f78059f0d96 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -585,7 +585,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
 
@@ -2642,6 +2642,24 @@ alter_table_cmd:
@@ -3773,8 +3791,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4123,6 +4143,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7218,6 +7251,14 @@ CommentStmt:
@@ -17842,7 +17883,6 @@ unreserved_keyword:
@@ -18146,6 +18186,7 @@ reserved_keyword:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

 
+			case AT_AddPeriod:
+				{
+					/*

 /*
 
+Oid
 
 /*

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 

 

 
+		/* print periods */

 
 
 /*

 

 #endif							/* TABLECMDS_H */
 
index eabadd47304..f2e34ff17b0 100644
@@ -2313,6 +2313,7 @@ typedef enum ObjectType
@@ -2401,6 +2402,8 @@ typedef enum AlterTableType
@@ -2682,11 +2685,11 @@ typedef struct VariableShowStmt
 
@@ -2695,6 +2698,7 @@ typedef struct CreateStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to create */
 	List	   *tableElts;		/* column definitions (list of ColumnDef) */
+	List	   *periods;		/* periods (list of PeriodDef nodes) */
 	List	   *inhRelations;	/* relations to inherit from (list of
 								 * RangeVar) */
 	PartitionBoundSpec *partbound;	/* FOR VALUES clause */
@@ -2708,6 +2712,31 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3401,6 +3430,7 @@ typedef struct IndexStmt

 

 

 
 
 # ----------

+-- Test with PERIODs
+

 

+--

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;

 

---

+--

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 31 Oct 2024 09:18:44 -0700

On 10/24/24 17:22, Paul Jungwirth wrote:
>> OVERLAPS indexes and (2) address some documentation lapses pointed out in jian he's feedback. 
>> Since all that is against the already-commited PK/UNIQUE/FK work, I've kept them separate here 
>> from the FOR PORTION OF etc patches. I've also added the logical replication problem to the v18 
>> Open Items wiki page.
> 

Hi Hackers,

Here are some more updates. I think everything is ready except for the final patch adding PERIODs, 
which still needs a little more work.

The biggest change here is a new patch to expose FOR PORTION OF details to plpgsql triggers via the 
new TG_PERIOD_NAME and TG_PERIOD_BOUNDS variables. These were already available to C functions (and 
we use them in the RI triggers), but now people will be able to access them via plpgsql as well.
One thing I don't love is that TG_PERIOD_BOUNDS is a string, because it needs to have a type at the 
time you create the function, and the type of the FOR PORTION OF column could be anything: 
daterange, tsrange, inetrange, textmultirange, whatever. If anyone has a better idea, I'm open to 
suggestions. This patch is not really essential, but I thought authors of plpgsql triggers might 
like to have it.

I also started working on allowing FOR PORTION OF in FDW updates/deletes (not included here). As far 
as I can see, nothing needs to change about the API, because the FOR PORTION OF details are simply 
passed as part of the node tree. We just have to remove the ereport that says FDW FOR PORTION OF is 
unsupported. Then within each FDW you have to use the new FOR PORTION OF node to do the right thing 
on the remote table. So I'm updating postgres_fdw to do that.

But beyond the *technical* need, do we need something to clue in FDW developers that these queries 
are a possibility? With no changes to postgres_fdw, a FOR PORTION OF update/delete gets executed; it 
just loses the FOR PORTION OF meaning. So we update/delete too much. That seems dangerous. It makes 
me *want* to change the API, so that developers must address the possibility. But I don't want to 
make things cluttered either. Any thoughts?

Rebased to fb7e27abfb.

Yours,

From 3129d14aeffaecd6d143d31c961b677b94836f57 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:02:12 -0700
Subject: [PATCH v43 1/8] Add WITHOUT OVERLAPS and PERIOD to ALTER TABLE
 reference docs

 
 
-- 
2.42.0

From 3ede02d6a8d97c85a9461e8fb208815b769f6e59 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 21 Oct 2024 13:12:46 -0700
Subject: [PATCH v43 2/8] Update conexclop docs for WITHOUT OVERLAPS

---

-- 
2.42.0

From c0b9cfed451a14ef6fa8d797f7d7471caba55d83 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 16 Oct 2024 11:06:40 -0700
Subject: [PATCH v43 3/8] Fix logical replication for temporal tables

index 4345b96de5e..51e280dda58 100644
@@ -16885,9 +16885,14 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode

 
 /*

 

From 3d44c483e1402b5000b734b6891e027f4eff6374 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v43 4/8] Add support funcs for FOR PORTION OF

 doc/src/sgml/gist.sgml                        | 156 +++++++++++++++-
 src/backend/utils/adt/rangetypes.c            | 166 ++++++++++++++++++
 src/test/regress/expected/multirangetypes.out | 116 ++++++++++++
 21 files changed, 684 insertions(+), 40 deletions(-)

index 20d3ab9109b..4b2e0ca90a5 100644
@@ -103,7 +103,7 @@ blvalidate(Oid opclassoid)
 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>

 	va_start(ap, maxargs);
 
 /*
index 558c6c18c34..c7de78dcd9e 100644
 
+/*
+ * multirange_without_portion - multirange minus but returning the result as a SRF,
index 2d94a6b8774..6f85e1f1bdf 100644
 
@@ -1213,6 +1215,170 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 	return false;
 }
 
+/*
+ * range_without_portion - subtraction but as a SRF to accommodate splits,
+ * with no result rows if the result would be empty.
+ */
+Datum
 
 Datum

 /*
 
 # gin

+-- without_portion

+-- without_portion

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

 .../postgres_fdw/expected/postgres_fdw.out    |  45 +-
 contrib/postgres_fdw/sql/postgres_fdw.sql     |  34 +
 doc/src/sgml/ref/create_publication.sgml      |   6 +
 src/test/regress/expected/for_portion_of.out  | 764 ++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql       | 597 ++++++++++++++
 51 files changed, 3271 insertions(+), 91 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f2bcd6aa98c..d7b93891ddb 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -57,11 +57,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,
@@ -88,10 +96,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
 -- ===================================================================
@@ -139,6 +154,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
 -- ===================================================================
@@ -210,7 +231,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
@@ -6125,6 +6147,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 372fe6dad15..61c075850dc 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -61,12 +61,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,
@@ -94,11 +102,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
@@ -153,6 +168,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
 -- ===================================================================
@@ -1499,6 +1522,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/sepgsql/proc.c b/contrib/sepgsql/proc.c
index 4899bc2a3e3..20120fd531f 100644
--- a/contrib/sepgsql/proc.c
+++ b/contrib/sepgsql/proc.c
@@ -161,7 +161,7 @@ sepgsql_proc_drop(Oid functionId)
 	 * check db_schema:{remove_name} permission
 	 */
 	object.classId = NamespaceRelationId;
-	object.objectId = get_func_namespace(functionId);
+	object.objectId = get_func_namespace(functionId, true);
 	object.objectSubId = 0;
 	audit_name = getObjectIdentity(&object, false);
 
diff --git a/contrib/unaccent/unaccent.c b/contrib/unaccent/unaccent.c
index 0217696aac1..e87a588a3ae 100644
--- a/contrib/unaccent/unaccent.c
+++ b/contrib/unaccent/unaccent.c
@@ -447,7 +447,7 @@ unaccent_dict(PG_FUNCTION_ARGS)
 		 * Use the "unaccent" dictionary that is in the same schema that this
 		 * function is in.
 		 */
-		Oid			procnspid = get_func_namespace(fcinfo->flinfo->fn_oid);
+		Oid			procnspid = get_func_namespace(fcinfo->flinfo->fn_oid, true);
 		const char *dictname = "unaccent";
 
 		dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index d2cac06fd76..1818f36b6d1 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -323,6 +323,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    for each row inserted, updated, or deleted.
   </para>
 
+  <para>
+   For a <command>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 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 7717855bc9e..6330fdff4c0 100644
--- a/doc/src/sgml/ref/delete.sgml
+++ b/doc/src/sgml/ref/delete.sgml
@@ -22,7 +22,9 @@ 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_or_period_name</replaceable> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</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 { * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] } [, ...] ]
@@ -54,6 +56,39 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>
index a9abaab9056..624514ffbfd 100644
 
+   <para>
 
+    Datum   fp_targetRange;     /* the range/multirange/etc from FOR PORTION OF */
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7c0fd63b2f0..c4f057982ee 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -4407,7 +4407,7 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 
index 51e280dda58..cf894b2298e 100644
@@ -12423,6 +12423,7 @@ validateForeignKeyConstraint(char *conname,

 

 

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

 #include "postgres.h"

 
+/*
+ * FPO_QueryHashEntry
+ */

 /*

 

+	/*

 

 #include "postgres.h"

@@ -59,10 +64,16 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*
index dd458182f02..7116dacce1d 100644
@@ -240,6 +240,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -536,6 +537,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -745,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER

@@ -864,12 +867,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -12305,6 +12311,20 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 
 using_clause:
@@ -12380,6 +12400,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->stmt_location = @$;
 					$$ = (Node *) n;
 				}
 
 set_clause_list:
@@ -13863,6 +13902,44 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+opt_alias:
+			AS ColId
+				{
@@ -14696,16 +14773,25 @@ opt_timezone:
 
+/*
@@ -17756,6 +17842,7 @@ unreserved_keyword:
@@ -18381,6 +18468,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
index c2806297aa4..8ae51017d43 100644
@@ -584,6 +584,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 			/*
@@ -1858,6 +1861,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 
 			/*
@@ -3151,6 +3157,8 @@ ParseExprKindName(ParseExprKind exprKind)
 
 			/*

 /*

 

 

 	/*
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *

 /*
 
+/*
+ * get_typname
+ *
 
 /*
index 182a6956bb0..41ef0605394 100644

+/*
+ * ForPortionOfState
+ *
+	Datum	fp_targetRange;		/* the range/multirange/etc from FOR PORTION OF */

 
 
+/*
+ * ForPortionOfClause

 
+/*----------
index 00000000000..19a4e9ac523
@@ -0,0 +1,764 @@
+-- DELETE ... RETURNING returns the deleted values (regardless of bounds)
+FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
+ [3,4) | [2018-02-01,2018-02-02) | three^3
+ [3,4) | [2018-02-03,2018-02-15) | three^3
+(17 rows)
index 1d903babd33..30d5d30f52f 100644
+-- test column-level privileges on the range used in FOR PORTION OF
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 

 # ----------
index 00000000000..e66a55fe014
@@ -0,0 +1,597 @@
+-- DELETE ... RETURNING returns the deleted values (regardless of bounds)
+FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03'
 
index 3f54b0f8f05..0688b3662e1 100644
 
+-- test column-level privileges on the range used in FOR PORTION OF

 
 -- temporal UNIQUE:

 

 

 

 

 

 

 

 

 

 
 # cleanup

 

 

 

 # cleanup

 

 

 
-- 
2.42.0

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

---

          <para>
 
          <para>
 
          <para>
 
 /*
- * FindFkPeriodOpers -
 
 /*
index cf894b2298e..dcd7f3d6972 100644

 
-	/*

 
@@ -12551,17 +12546,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12612,17 +12616,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

 

+/*

 	ReleaseSysCache(tup);
 
 	/*

 

 
 
 explain (costs off)

 explain (costs off)

 

+--
 
+--

 

 

 

 

 

 

 

 

 

-- 
2.42.0

From 92b33112ed250d668ce256a2eb23e00927bff197 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 29 Oct 2024 18:54:37 -0700
Subject: [PATCH v43 7/8] 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 triggers functions to
access the same information, using the new TG_PORTION_COLUMN and
TG_PORTION_TARGET variables.
---
 doc/src/sgml/plpgsql.sgml                    | 24 +++++++++++
 src/pl/plpgsql/src/pl_comp.c                 | 27 +++++++++++++
 src/pl/plpgsql/src/pl_exec.c                 | 32 +++++++++++++++
 src/pl/plpgsql/src/plpgsql.h                 |  2 +
 src/test/regress/expected/for_portion_of.out | 42 ++++++++++----------
 src/test/regress/sql/for_portion_of.sql      | 14 ++++---
 6 files changed, 115 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 78e4983139b..31bd824ed3d 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -4245,6 +4245,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 5633e3c7905..650d882daa6 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -717,6 +717,33 @@ do_compile(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 to 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 86c5bd324a9..ec93789337f 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -1369,6 +1369,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 						PLpgSQL_var *var)
 {
 	MemoryContext oldcontext;
+	ForPortionOfState *fpo;
 
 	if (var->promise == PLPGSQL_PROMISE_NONE)
 		return;					/* nothing to do */
@@ -1500,6 +1501,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:
+			fpo = estate->trigdata->tg_temporal;
+
+			if (estate->trigdata == NULL)
+				elog(ERROR, "trigger promise is not in a trigger function");
+			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 50c3b28472b..bf66340151a 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -84,6 +84,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 19a4e9ac523..4f9ee28f078 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -512,16 +512,18 @@ RETURNS TRIGGER LANGUAGE plpgsql AS
 $$
 BEGIN
   IF TG_OP = 'INSERT' THEN
-    RAISE NOTICE '%: % %, NEW table = %',
-      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, NEW table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
+      (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
   ELSIF TG_OP = 'UPDATE' THEN
-    RAISE NOTICE '%: % %, OLD table = %, NEW table = %',
-      TG_NAME, TG_OP, TG_LEVEL,
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, OLD table = %, NEW table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
       (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table),
       (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
   ELSIF TG_OP = 'DELETE' THEN
-    RAISE NOTICE '%: % %, OLD table = %',
-      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, OLD table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
+      (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
   END IF;
   RETURN NULL;
 END;
@@ -554,29 +556,29 @@ 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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
-NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
-NOTICE:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
-NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
-NOTICE:  for_portion_of_test_update_trig: UPDATE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
-NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
+NOTICE:  for_portion_of_test_insert_trig: INSERT FOR PORTION OF <NULL> (<NULL>) ROW, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT FOR PORTION OF <NULL> (<NULL>) STATEMENT, NEW table = ("[1,2)","[2018-01-01,2018-01-15)",one)
+NOTICE:  for_portion_of_test_insert_trig: INSERT FOR PORTION OF <NULL> (<NULL>) ROW, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT FOR PORTION OF <NULL> (<NULL>) STATEMENT, NEW table = ("[1,2)","[2019-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_update_trig: UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01)
+NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE FOR PORTION OF valid_at ([2018-01-15,2019-01-01)) STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
-NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
-NOTICE:  for_portion_of_test_delete_trig: DELETE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
-NOTICE:  for_portion_of_test_delete_trig_stmt: DELETE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig: INSERT FOR PORTION OF <NULL> (<NULL>) ROW, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT FOR PORTION OF <NULL> (<NULL>) STATEMENT, NEW table = ("[1,2)","[2018-01-21,2020-01-01)",one)
+NOTICE:  for_portion_of_test_delete_trig: DELETE FOR PORTION OF valid_at ((,2018-01-21)) ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
+NOTICE:  for_portion_of_test_delete_trig_stmt: DELETE FOR PORTION OF valid_at ((,2018-01-21)) STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one)
 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:  for_portion_of_test_insert_trig: INSERT ROW, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
-NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT STATEMENT, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
-NOTICE:  for_portion_of_test_update_trig: UPDATE ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
-NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+NOTICE:  for_portion_of_test_insert_trig: INSERT FOR PORTION OF <NULL> (<NULL>) ROW, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  for_portion_of_test_insert_trig_stmt: INSERT FOR PORTION OF <NULL> (<NULL>) STATEMENT, NEW table = ("[1,2)","[2018-01-02,2020-01-01)",one)
+NOTICE:  for_portion_of_test_update_trig: UPDATE FOR PORTION OF valid_at ((,2018-01-02)) ROW, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
+NOTICE:  for_portion_of_test_update_trig_stmt: UPDATE FOR PORTION OF valid_at ((,2018-01-02)) STATEMENT, OLD table = ("[1,2)","[2018-01-01,2020-01-01)",one), NEW table = ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01)
 ROLLBACK;
 -- Test with multiranges
 CREATE TABLE for_portion_of_test2 (
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index e66a55fe014..cd1645a436f 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -396,16 +396,18 @@ RETURNS TRIGGER LANGUAGE plpgsql AS
 $$
 BEGIN
   IF TG_OP = 'INSERT' THEN
-    RAISE NOTICE '%: % %, NEW table = %',
-      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, NEW table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
+      (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
   ELSIF TG_OP = 'UPDATE' THEN
-    RAISE NOTICE '%: % %, OLD table = %, NEW table = %',
-      TG_NAME, TG_OP, TG_LEVEL,
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, OLD table = %, NEW table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
       (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table),
       (SELECT string_agg(new_table::text, ', ' ORDER BY id) FROM new_table);
   ELSIF TG_OP = 'DELETE' THEN
-    RAISE NOTICE '%: % %, OLD table = %',
-      TG_NAME, TG_OP, TG_LEVEL, (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
+    RAISE NOTICE '%: % FOR PORTION OF % (%) %, OLD table = %',
+      TG_NAME, TG_OP, TG_PERIOD_NAME, TG_PERIOD_BOUNDS, TG_LEVEL,
+      (SELECT string_agg(old_table::text, ', ' ORDER BY id) FROM old_table);
   END IF;
   RETURN NULL;
 END;
-- 
2.42.0

From 0c5e7e57c88d9a67dbed3e14023e3bfe6175eefa Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v43 8/8] Add PERIODs

 doc/src/sgml/plpgsql.sgml                     |    2 +-
 src/backend/parser/analyze.c                  |  108 +-
 src/test/regress/expected/for_portion_of.out  |   58 +
 src/test/regress/expected/privileges.out      |   29 +
 .../regress/expected/without_overlaps.out     | 4685 ++++++++++++++++-
 src/test/regress/sql/for_portion_of.sql       |   50 +
 src/test/regress/sql/privileges.sql           |   28 +
 src/test/regress/sql/without_overlaps.sql     | 3223 +++++++++++-
 64 files changed, 10789 insertions(+), 204 deletions(-)

index d2b559376f0..a93b082b59a 100644
 
+     <row>

 
 
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 31bd824ed3d..e2c8fd1c0f2 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -4250,7 +4250,7 @@ ASSERT <replaceable class="parameter">condition</replaceable> <optional> , <repl
      <term><varname>TG_PERIOD_NAME</varname> <type>text</type></term>
      <listitem>
       <para>
-       the column name used in a <literal>FOR PORTION OF</literal> clause,
+       the column/period name used in a <literal>FOR PORTION OF</literal> clause,
        or else <symbol>NULL</symbol>.
       </para>
      </listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index d8fa86dba16..43298aa29fd 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -61,6 +61,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
     VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
     DROP CONSTRAINT [ IF EXISTS ]  <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+    ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+    DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
     DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -114,10 +116,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM

 

 
+  <para>

 
      <para>
 
   <para>
 
   <para>
index 624514ffbfd..294e4a945d1 100644
 
     Datum   fp_targetRange;     /* the range/multirange/etc from FOR PORTION OF */
 } ForPortionOfState;
 </programlisting>

+					/*
 
+		case PeriodRelationId:
index c54a543c536..92bab488237 100644
@@ -2064,6 +2065,80 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 
+/*

 /*
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case PeriodRelationId:
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index a586d246ece..bc7a1ce5ecc 100644
@@ -2169,6 +2169,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2253,6 +2254,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index dcd7f3d6972..43c1a5ae3de 100644

+	/*
 
+	/*
 
 	/*
 
+/*

+/*----------
+ * MergePeriods

 
 /*

 
+/*
 
+/*
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -13647,6 +14482,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 
 				/*
@@ -13736,6 +14581,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -15630,7 +16484,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 
 		r = DatumGetRangeTypeP(src->fp_targetRange);
 		typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 2bd49eb55e6..9391b30ec7b 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -163,7 +163,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			}

 			/* Make the new view columns visible */
 			CommandCounterIncrement();
@@ -195,7 +195,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		atcmds = list_make1(atcmd);

 		/*

 
 
index 8d6876a41f9..18fd3c43e03 100644
 
+	/*
 
@@ -1496,16 +1551,51 @@ transformForPortionOfClause(ParseState *pstate,

-		/* Mark the range column as requiring update permissions */
-		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
-													  range_attno - FirstLowInvalidHeapAttributeNumber);
+		if (result->startVar)
+		{
+			Expr *boundTLEExpr;
+
+			/* set the start column */
+			boundTLEExpr = (Expr *) makeFuncCall(SystemFuncName("lower"),
+					list_make1(rangeTLEExpr),
+					COERCE_EXPLICIT_CALL,
+					forPortionOf->location);
+			boundTLEExpr = (Expr *) transformExpr(pstate, (Node *) boundTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(boundTLEExpr, start_attno, startcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* set the end column */
+			boundTLEExpr = (Expr *) makeFuncCall(SystemFuncName("upper"),
+					list_make1(rangeTLEExpr),
+					COERCE_EXPLICIT_CALL,
+					forPortionOf->location);
+			boundTLEExpr = (Expr *) transformExpr(pstate, (Node *) boundTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(boundTLEExpr, end_attno, endcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/*
+			 * Mark the start/end columns as requiring update permissions.
+			 * As usual, we don't check permissions for the GENERATED column.
+			 */
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  start_attno - FirstLowInvalidHeapAttributeNumber);
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  end_attno - FirstLowInvalidHeapAttributeNumber);
+		}
+		else
+		{
+			rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
+			tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* Mark the range column as requiring update permissions */
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  range_attno - FirstLowInvalidHeapAttributeNumber);
+		}
 	}
 	else
 		result->rangeTargetList = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7116dacce1d..f909e250e38 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -581,7 +581,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
 
@@ -2638,6 +2638,24 @@ alter_table_cmd:
@@ -3769,8 +3787,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4119,6 +4139,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7214,6 +7247,14 @@ CommentStmt:
@@ -17838,7 +17879,6 @@ unreserved_keyword:
@@ -18142,6 +18182,7 @@ reserved_keyword:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

 
+			case AT_AddPeriod:
+				{
+					/*

 /*
 
+Oid
 
 /*
index d8c6330732e..7b917cc9c73 100644
@@ -6787,6 +6787,7 @@ getTables(Archive *fout, int *numTables)
@@ -6864,6 +6865,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -7001,6 +7010,7 @@ getTables(Archive *fout, int *numTables)
@@ -7084,6 +7094,7 @@ getTables(Archive *fout, int *numTables)
@@ -8717,7 +8728,7 @@ getTransforms(Archive *fout)
@@ -8767,6 +8778,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8781,7 +8794,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9143,15 +9157,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9173,6 +9208,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9192,12 +9228,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9256,6 +9293,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10484,6 +10595,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16042,6 +16155,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16050,7 +16190,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16296,7 +16436,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 */
 			resetPQExpBuffer(extra);
 			firstitem = true;
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -16602,7 +16742,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18675,6 +18815,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:

 

+		/* print periods */

 
 
 /*

 

 #endif							/* TABLECMDS_H */
index 41ef0605394..20113ba8903 100644
 
 	Datum	fp_targetRange;		/* the range/multirange/etc from FOR PORTION OF */

+
+/* ----------
 
index 97cf52d133c..f1bf2f3e353 100644
@@ -182,6 +182,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
index 4f9ee28f078..c7c36924e16 100644
@@ -612,6 +612,64 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
 
+-- Setting the start column fails:
+SET valid_from = '2018-02-01'
+ERROR:  multiple assignments to same column "valid_from"
+-- Setting the end column fails:
+SET valid_til = '2018-02-01'
+ERROR:  multiple assignments to same column "valid_til"
+-- Setting the generated column fails:
+SET valid_at = '[2018-02-01,2018-02-03)'
+LINE 3: SET valid_at = '[2018-02-01,2018-02-03)'
index 30d5d30f52f..88af5c44650 100644
@@ -1056,6 +1056,35 @@ 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)';
 DROP TABLE t1;
+-- test column-level privileges on the PERIOD used in FOR PORTION OF
+  valid_from timestamp,
+  valid_til timestamp,
+GRANT SELECT ON t1 TO regress_priv_user4;
+GRANT SELECT ON t1 TO regress_priv_user5;
+GRANT UPDATE (c1, valid_from, valid_til) ON t1 TO regress_priv_user4;
+GRANT UPDATE (c1, valid_from, valid_til, valid_at) ON t1 TO regress_priv_user5;
+SET SESSION AUTHORIZATION regress_priv_user4;
+SET SESSION AUTHORIZATION regress_priv_user5;
index 88ab309e135..af0b2f644d7 100644

 
@@ -933,6 +1453,37 @@ ERROR:  column "valid_at" is in a primary key
 ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
 ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
 ALTER TABLE temporal3 DROP COLUMN valid_thru;
+--------+-----------+-----------+----------+---------
+ERROR:  cannot specify USING when altering type of generated column
+DETAIL:  Column "valid_at" is a generated column.
+ERROR:  cannot drop column valid_at of table temporal3 because period valid_at on table temporal3 requires it
+HINT:  You can drop period valid_at on table temporal3 instead.
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at;
 -- test PARTITION BY for ranges
@@ -1405,18 +1956,196 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
   CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
     REFERENCES temporal3 (id, PERIOD valid_at)
@@ -1429,6 +2158,32 @@ ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
 NOTICE:  drop cascades to constraint temporal_fk_rng2rng_fk on table temporal_fk_rng2rng
+-- can't drop a PERIOD referenced by an FK, unless with CASCADE
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at;
+ERROR:  cannot drop period valid_at on table temporal3 because other objects depend on it
+DETAIL:  constraint temporal_fk_per2per_fk on table temporal_fk_per2per depends on column valid_at of table temporal3
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at CASCADE;
 -- test FOREIGN KEY, range references range
@@ -1458,6 +2213,18 @@ CREATE TABLE temporal_fk_rng2rng (
@@ -3706,102 +4473,3832 @@ SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, val

-  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_parent_id_valid_at_fkey1" 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".
@@ -4409,4 +8906,18 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';

 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index cd1645a436f..45594668819 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -489,6 +489,56 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;

+-- Test with PERIODs
+
+CREATE TABLE for_portion_of_test2 (
+  id int4range NOT NULL,
+  valid_from date,
+  valid_til date,
+  name text NOT NULL,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test2
+VALUES
+('[1,2)', '2018-01-02', '2018-02-03', 'one'),
+('[1,2)', '2018-02-04', '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', null, '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)';
+
+-- Setting the start column fails:
+UPDATE for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+SET valid_from = '2018-02-01'
+WHERE id = '[1,2)';
+
+-- Setting the end column fails:
+UPDATE for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+SET valid_til = '2018-02-01'
+WHERE id = '[1,2)';
+
+-- Setting the generated column fails:
+UPDATE for_portion_of_test2
+FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10'
+SET valid_at = '[2018-02-01,2018-02-03)'
+WHERE id = '[1,2)';
+
+SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;
+
+DROP TABLE for_portion_of_test2;
+
 -- Test with a custom range type
 
index 0688b3662e1..fbdea52ce94 100644
@@ -741,6 +741,34 @@ UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2
 DROP TABLE t1;
 
+-- test column-level privileges on the PERIOD used in FOR PORTION OF
+SET SESSION AUTHORIZATION regress_priv_user1;
+CREATE TABLE t1 (
+  c1 int4range,
+  valid_from timestamp,
+  valid_til timestamp,
+  PERIOD FOR valid_at (valid_from, valid_til),
+	CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS)
+);
+GRANT SELECT ON t1 TO regress_priv_user2;
+GRANT SELECT ON t1 TO regress_priv_user3;
+GRANT SELECT ON t1 TO regress_priv_user4;
+GRANT SELECT ON t1 TO regress_priv_user5;
+GRANT UPDATE (c1) ON t1 TO regress_priv_user2;
+GRANT UPDATE (c1, valid_at) ON t1 TO regress_priv_user3;
+GRANT UPDATE (c1, valid_from, valid_til) ON t1 TO regress_priv_user4;
+GRANT UPDATE (c1, valid_from, valid_til, valid_at) ON t1 TO regress_priv_user5;
+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_user4;
+UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)';
+SET SESSION AUTHORIZATION regress_priv_user5;
+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;
+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/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 997018fd05c..78c61dacfcd 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -91,6 +91,31 @@ CREATE TABLE temporal_rng2 (
 SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';
 SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk';

 
 
+--
@@ -662,6 +972,22 @@ ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
 ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
 ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru;
 ALTER TABLE temporal3 DROP COLUMN valid_thru;
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at;
+\d temporal3
 DROP TABLE temporal3;
 
 --
@@ -962,6 +1288,121 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;
@@ -985,6 +1426,29 @@ ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
 
+-- can't drop a PERIOD referenced by an FK, unless with CASCADE
+CREATE TABLE temporal3 (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal3 (id, PERIOD valid_at)
+);
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at;
+ALTER TABLE temporal3 DROP PERIOD FOR valid_at CASCADE;
+DROP TABLE temporal_fk_per2per;
+DROP TABLE temporal3;
+
 --
 -- test FOREIGN KEY, range references range
 --
@@ -1015,6 +1479,7 @@ CREATE TABLE temporal_fk_rng2rng (
   CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
     REFERENCES temporal_rng (id, PERIOD valid_at)
 );
+\d temporal_fk_rng2rng
 DROP TABLE temporal_fk_rng2rng;
 
 -- with mismatched PERIOD columns:
@@ -2673,54 +3138,2725 @@ INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VA
 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;

 
 
---

@@ -3181,4 +6317,19 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 
+--

---

From: Sam Gabrielsson <[email-masked]>
Date: Mon, 04 Nov 2024 22:16:37 +0100

Foreign key violation errors are incorrectly raised in a few cases for a 
temporal foreign key with default ON UPDATE NO ACTION. Test is based on 
the commited v39 patches (used a snapshot version of PG18 devel 
available from PGDG).

If there exists a single referencing row for a foreign key (with default 
ON UPDATE NO ACTION) with a range such as:

                  c          d
                  |----------|

and a single row in the referenced table, and the referenced row's range 
is updated as in one of the following cases:

  a           b   c          d   e           f
  X>>>>>>>>>>>|==============================|     ERROR 1: [a,f) updated 
to [b,f) or
  |==============================|<<<<<<<<<<<X              [a,f) updated 
to [a,e)
              |==================|<<<<<<<<<<<<<<<  ERROR 2: [b,)  updated 
to [b,e)
  X>>>>>>>>>>>|==================================  ERROR 3: [a,)  updated 
to [b,)

then an error is incorrectly raised (also, if the referencing range is 
[c,) instead of [c,d), then the last case also fails). See SQL-code 
below for how to reproduce the errors.

---

   valid_at daterange,

CREATE TABLE temporal_fk_rng2rng (
   id int4range,
   valid_at daterange,
   parent_id int4range,
   CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT 
OVERLAPS),
   CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD 
valid_at) REFERENCES temporal_rng
);

-- ERROR 1

INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[1,2)', daterange('2018-01-01', '2018-03-01'));
INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[2,3)', daterange('2018-01-15', '2018-02-01'), '[1,2)');
-- 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)=([1,2), [2018-01-01,2018-03-01)) is still 
referenced from table "temporal_fk_rng2rng".
UPDATE temporal_rng
SET valid_at = daterange('2018-01-05', '2018-03-01')
WHERE id = '[1,2)' AND valid_at = daterange('2018-01-01', '2018-03-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)=([1,2), [2018-01-01,2018-03-01)) is still 
referenced from table "temporal_fk_rng2rng".
UPDATE temporal_rng
SET valid_at = daterange('2018-01-01', '2018-02-15')
WHERE id = '[1,2)' AND valid_at = daterange('2018-01-01', '2018-03-01');

-- ERROR 2

TRUNCATE temporal_rng, temporal_fk_rng2rng;

INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[1,2)', daterange('2018-01-05', NULL));
INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[2,3)', daterange('2018-01-15', '2018-02-01'), '[1,2)');
-- 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)=([1,2), [2018-01-05,)) is still 
referenced from table "temporal_fk_rng2rng".
UPDATE temporal_rng
SET valid_at = daterange('2018-01-05', '2018-02-15')
WHERE id = '[1,2)' AND valid_at = daterange('2018-01-05', NULL);

-- ERROR 3

TRUNCATE temporal_rng, temporal_fk_rng2rng;

INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[1,2)', daterange('2018-01-01', NULL));
INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
   ('[2,3)', daterange('2018-01-15', '2018-02-01'), '[1,2)');
-- 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)=([1,2), [2018-01-01,)) is still 
referenced from table "temporal_fk_rng2rng".
UPDATE temporal_rng
SET valid_at = daterange('2018-01-05', NULL)
WHERE id = '[1,2)' AND valid_at = daterange('2018-01-01', NULL);

---

I think the problem is the check in ri_restrict:

   SELECT 1 FROM [ONLY] <fktable> x WHERE $1 = fkatt1 [AND ...]
        FOR KEY SHARE OF x

it will be performed in the NO ACTION case when ri_Check_Pk_Match 
returns false, and it'll then incorrectly assume that the presence of a 
referencing row in the <fktable> is an error. However, ri_Check_Pk_Match 
only tests wheter a temporal primary key's old range is contained by the 
multirange that includes its new updated range. If that's true, then all 
references are necessarily still valid. However, even if it is not 
contained, all references can still be valid. So, only testing for the 
presence of a referencing row is not enough.

For example, for ERROR1, the range [a,f) is updated to [b,f):

  a           b   c          d               f
  X>>>>>>>>>>>|==============================|

Clearly the old range:

  a               c          d               f
  |==========================================|

is no longer contained by (the multirange returned by range_agg of) the 
new range:

              b   c          d               f
              |==============================|

So ri_Check_Pk_Match returns false. Though the row in the referencing 
table:

                  c          d
                  |----------|

only specifies the range [c,d), so the temporal referential integrity 
still holds. However, the ri_restrict test will find a row in the 
referencing table and because of that raise an error.

In the temporal NO ACTION case something similar to this (though with 
appropriate locks) could perhaps be tested in ri_restrict (when 
ri_Check_Pk_Match returns false):

   SELECT 1
   FROM (SELECT range_agg(pkperiodatt) AS r
       FROM <pktable>
       WHERE pkatt1 = $1 [AND ...]
       AND pkperiodatt && $n) AS pktable,
     (SELECT fkperiodatt AS r
       FROM <fktable>
       WHERE fkatt1 = $1 [AND ...]
       AND fkperiodatt && $n) AS fktable
   WHERE NOT fktable.r <@ pktable.r

/Sam

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 13 Nov 2024 11:11:37 +0100

I have committed the documentation patches

v43-0001-Add-WITHOUT-OVERLAPS-and-PERIOD-to-ALTER-TABLE-r.patch
v43-0002-Update-conexclop-docs-for-WITHOUT-OVERLAPS.patch

For the logical replication fixes

v43-0003-Fix-logical-replication-for-temporal-tables.patch

can you summarize what the issues currently are?  Is it currently 
broken, or just not working as well as it could?

AFAICT, there might be two separate issues.  One is that you can't use a 
temporal index as replica identity, because ALTER TABLE rejects it.  The 
other is that a subscriber fails to make use of a replica identity 
index, because it uses the wrong strategy numbers.

This conditional is really hard to understand:

+       /*
+        * The AM must support uniqueness, and the index must in fact be 
unique.
+        * If we have a WITHOUT OVERLAPS constraint (identified by 
uniqueness +
+        * exclusion), we can use that too.
+        */
+       if ((!indexRel->rd_indam->amcanunique ||
+               !indexRel->rd_index->indisunique) &&
+               !(indexRel->rd_index->indisunique && 
indexRel->rd_index->indisexclusion))

Why can we have a indisunique index when the AM is not amcanunique?  Are 
we using the fields wrong?

-       return get_equal_strategy_number_for_am(am);
+       /* For GiST indexes we need to ask the opclass what strategy 
number to use. */
+       if (am == GIST_AM_OID)
+               return GistTranslateStratnum(opclass, 
RTEqualStrategyNumber);
+       else
+               return get_equal_strategy_number_for_am(am);

This code should probably be pushed into 
get_equal_strategy_number_for_am().  That function already has a switch 
based on index AM OIDs.  Also, there are other callers of 
get_equal_strategy_number_for_am(), which also might want to become 
aware of GiST support.

For the new test file, remember to add it to 
src/test/subscription/meson.build.

Also, maybe add a introductory comment in the test file to describe 
generally what it's trying to test.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 13 Nov 2024 11:50:54 +0100

A quick comment on the patch

v43-0005-Add-UPDATE-DELETE-FOR-PORTION-OF.patch

regarding the code in transformForPortionOfClause() and the additions 
you made to lsyscache.c:

What you are doing is taking a type OID and a function OID and then 
converting them back to name and namespace and then building a node and 
then feeding that node back through the parse analysis transformation. 
This all seems quite fishy and cumbersome.  I think instead of building 
a FuncCall and transforming it, try to build a FuncExpr directly.  Then 
you wouldn't need these new helper functions, which would also reduce 
the surface area of your patch.

Additional mini-comment:

#include "utils/rangetypes.h"

in src/include/nodes/execnodes.h appears to be unnecessary (but it is 
then required in src/backend/commands/trigger.c).

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 13 Nov 2024 11:53:06 +0100

I committed a few fixes in this area today.  Has everything here been 
addressed?

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 14 Nov 2024 09:25:15 -0800

> v43-0002-Update-conexclop-docs-for-WITHOUT-OVERLAPS.patch

Thanks!

> can you summarize what the issues currently are?  Is it currently broken, or just not working as 
> well as it could?
> 
> AFAICT, there might be two separate issues.  One is that you can't use a temporal index as replica 
> identity, because ALTER TABLE rejects it.  The other is that a subscriber fails to make use of a 
> replica identity index, because it uses the wrong strategy numbers.

Correct, there are two issues this commit fixes:

On the publisher side: You can use REPLICA IDENTITY DEFAULT with a temporal PK/UNIQUE index. There 
is no validation step, and sending the changes works fine. But REPLICA IDENTITY USING INDEX fails 
because the validation step rejects the non-btree index.

Then on the subscriber side, we are not applying changes correctly, because we assume the strategy 
numbers are btree numbers.

> Why can we have a indisunique index when the AM is not amcanunique?  Are we using the fields wrong?
> 
> This code should probably be pushed into get_equal_strategy_number_for_am().  That function already 
> has a switch based on index AM OIDs.  Also, there are other callers of 
> get_equal_strategy_number_for_am(), which also might want to become aware of GiST support.
> 
> For the new test file, remember to add it to src/test/subscription/meson.build.
> 
> Also, maybe add a introductory comment in the test file to describe generally what it's trying to test.

Okay, I'll make these changes and re-send the patch.

Yours,

---

From: Matthias van de Meent <[email-masked]>
Date: Thu, 14 Nov 2024 19:14:18 +0100

> we using the fields wrong?

I called this issue out earlier this year: amcanunique implies
btree-style uniqueness, and allows CREATE UNIQUE INDEX. However, that
IndexAmRoutine field seems to be ignored for indexes that are created
to back temporal unique constraints, which thus get
indrel->indisunique = true. This causes interesting issues when you
look at the index catalog and errors: there are indexes with
indisunique using gist, but CREATE UNIQUE INDEX USING gist (...)
throws the nice "access method "gist" does not support unique indexes"
error.

It'd be nice if there was a better internal API to describe what types
of uniqueness each index supports, so CREATE UNIQUE INDEX could work
with gist for WITHOUT OVERLAPS, and these WITHOUT OVERLAPS unique
indexes could be attached to primary keys without taking O(>=
tablesize) of effort.

Kind regards,

Matthias van de Meent
Neon (https://neon.tech)

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 21 Nov 2024 13:56:36 +0100

On 14.11.24 18:25, Paul Jungwirth wrote:
>> AFAICT, there might be two separate issues.  One is that you can't use 
>> a temporal index as replica identity, because ALTER TABLE rejects it.  
>> The other is that a subscriber fails to make use of a replica identity 
> On the publisher side: You can use REPLICA IDENTITY DEFAULT with a 
> temporal PK/UNIQUE index. There is no validation step, and sending the 
> changes works fine. But REPLICA IDENTITY USING INDEX fails because the 
> validation step rejects the non-btree index.

Ok, I have committed the fix for this, and I'll continue working through 
the rest of the patches.

> Then on the subscriber side, we are not applying changes correctly, 
> because we assume the strategy numbers are btree numbers.

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 21 Nov 2024 10:35:58 -0600

On Thu, Nov 21, 2024 at 01:56:36PM +0100, Peter Eisentraut wrote:
> Ok, I have committed the fix for this, and I'll continue working through the
> rest of the patches.

nitpick: I think this one needs a pgindent [0].

[0] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=koel&dt=2024-11-21%2013%3A49%3A02

-- 
nathan

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 21 Nov 2024 16:30:06 -0800

Here are updated patches. I left off the final PERIODs patch because it still has some conflicts
with the new NOT NULL constraints work. I'll soon an update with that soon.

On 11/21/24 04:56, Peter Eisentraut wrote:
> Ok, I have committed the fix for this, and I'll continue working through the rest of the patches.

Thanks!

> get_equal_strategy_number_for_am(), which also might want to become aware of GiST support.

Done. The reason I didn't do this before is because we need the opclass, not just the am. I put an
explanation about that into the function comment. If that's a problem I can undo the change.

Done.

On 11/13/24 02:50, Peter Eisentraut wrote:
> regarding the code in transformForPortionOfClause() and the additions you made to lsyscache.c:
>
> What you are doing is taking a type OID and a function OID and then converting them back to name and
> namespace and then building a node and then feeding that node back through the parse analysis
> transformation. This all seems quite fishy and cumbersome.  I think instead of building a FuncCall
> and transforming it, try to build a FuncExpr directly.  Then you wouldn't need these new helper
> functions, which would also reduce the surface area of your patch.

Okay, that makes sense. I changed that for the FOR PORTION OF bounds expression. I still need to get
the rangetype name and namespace to look up its constructor function. Even if I built another FuncExpr
myself there, I don't see any way to get the constructor oid without calling SearchSysCache3(PROCNAMEARGSNSP, ...).
So I'm still doing type oid -> type name -> func oid.

> in src/include/nodes/execnodes.h appears to be unnecessary (but it is then required in src/backend/
> commands/trigger.c).

Done.

On 11/13/24 02:53, Peter Eisentraut wrote:
> I committed a few fixes in this area today.  Has everything here been addressed?

Yes, everything here was addressed in my v41 patches (sent 2024-10-21).

Rebased to 4c4aaa19a6.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From bcd5720248133962239027aee07b39fcef13ec23 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 16 Oct 2024 11:06:40 -0700
Subject: [PATCH v45 1/8] Fix logical replication for temporal tables

A WITHOUT OVERLAPS primary key or unique constraint should work as a
REPLICA IDENTITY, since it guarantees uniqueness. But subscribers
applying logical decoding messages got confused because they tried to
look up the equals operator based on btree strategy numbers. For GiST
indexes we can use the stratnum GiST support function.

Author: Paul Jungwirth
---
 src/backend/executor/execReplication.c     |  22 +-
 src/backend/replication/logical/relation.c |   2 +-
 src/include/executor/executor.h            |   2 +-
 src/test/subscription/meson.build          |   1 +
 src/test/subscription/t/034_temporal.pl    | 674 +++++++++++++++++++++
 5 files changed, 692 insertions(+), 9 deletions(-)
 create mode 100644 src/test/subscription/t/034_temporal.pl

diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 54025c9f150..18b4d086889 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
@@ -38,16 +39,18 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
 						 TypeCacheEntry **eq);
 
 /*
- * Returns the fixed strategy number, if any, of the equality operator for the
+ * Returns the strategy number, if any, of the equality operator for the
  * given index access method, otherwise, InvalidStrategy.
  *
- * Currently, only Btree and Hash indexes are supported. The other index access
- * methods don't have a fixed strategy for equality operation - instead, the
- * support routines of each operator class interpret the strategy numbers
- * according to the operator class's definition.
+ * Btree and Hash indexes are supported without an opclass. The other index
+ * access methods don't have a fixed strategy for equality operation - instead,
+ * the support routines of each operator class interpret the strategy numbers
+ * according to the operator class's definition. A GiST opclass may be able
+ * to communicate its preferred strategy number for equality, so we try
+ * asking.
  */
 StrategyNumber
-get_equal_strategy_number_for_am(Oid am)
+get_equal_strategy_number_for_am(Oid am, Oid opclass)
 {
 	int			ret;
 
@@ -59,6 +62,9 @@ get_equal_strategy_number_for_am(Oid am)
 		case HASH_AM_OID:
 			ret = HTEqualStrategyNumber;
 			break;
+		case GIST_AM_OID:
+			ret	= GistTranslateStratnum(opclass, RTEqualStrategyNumber);
+			break;
 		default:
 			/* XXX: Only Btree and Hash indexes are supported */
 			ret = InvalidStrategy;
@@ -77,7 +83,7 @@ get_equal_strategy_number(Oid opclass)
 {
 	Oid			am = get_opclass_method(opclass);
 
-	return get_equal_strategy_number_for_am(am);
+	return get_equal_strategy_number_for_am(am, opclass);
 }
 
 /*
@@ -134,6 +140,8 @@ build_replindex_scan_key(ScanKey skey, Relation rel, Relation idxrel,
 
 		operator = get_opfamily_member(opfamily, optype,
 									   optype,
diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index f5a0ef2bd9d..59161d2b176 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -806,7 +806,7 @@ IsIndexUsableForReplicaIdentityFull(IndexInfo *indexInfo, AttrMap *attrmap)
 	AttrNumber	keycol;
 
 	/* Ensure that the index access method has a valid equal strategy */
-	if (get_equal_strategy_number_for_am(indexInfo->ii_Am) == InvalidStrategy)
+	if (get_equal_strategy_number_for_am(indexInfo->ii_Am, InvalidOid) == InvalidStrategy)
 		return false;
 
 	/* The index must not be a partial index */
index 69c3ebff00a..d5a17609538 100644
@@ -658,7 +658,7 @@ extern void check_exclusion_constraint(Relation heap, Relation index,
-extern StrategyNumber get_equal_strategy_number_for_am(Oid am);
+extern StrategyNumber get_equal_strategy_number_for_am(Oid am, Oid opclass);
 extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 										 LockTupleMode lockmode,
 										 TupleTableSlot *searchslot,
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index c591cd7d619..b2395e7b57e 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -40,6 +40,7 @@ tests += {
       't/031_column_list.pl',
       't/032_subscribe_use_index.pl',
       't/033_run_as_table_owner.pl',
+      't/034_temporal.pl',
       't/100_bugs.pl',
index 00000000000..bff3b5aa0e3
@@ -0,0 +1,674 @@
+#
+# A table can use a temporal PRIMARY KEY or UNIQUE index as its REPLICA IDENTITY.
+# This is a GiST index (not B-tree) and its last element uses WITHOUT OVERLAPS.
+# That element restricts other rows with overlaps semantics instead of equality,
+# but it is always at least as restrictive as a normal non-null unique index.
+# Therefore we can still apply logical decoding messages to the subscriber.

From 39d12053961db0066d87d34ab6a8f6054028e720 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v45 2/8] Add without_portion GiST support proc

This new support proc is used by RESTRICT foreign keys to compute the
portion of history that was lost when the application-time bounds of a
record change. This commit defines implementations for ranges and
multiranges.

The procs return SETOF their input type and work like minus but don't
fail on splits. The results never contain empty elements.

When we implement FOR PORTION OF, we will use these procs simiarly: to
compute leftovers that weren't touched by the UPDATE/DELETE.

Author: Paul Jungwirth
---
 doc/src/sgml/gist.sgml                        | 107 ++++++++++-
 src/backend/access/gist/gistvalidate.c        |  25 +--
 src/include/catalog/pg_amproc.dat             |   6 +
 21 files changed, 613 insertions(+), 40 deletions(-)

index 638d912dc2d..1fbddf1bfb0 100644
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and seven that are optional.
+   <acronym>GiST</acronym> must provide, and eight that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -294,6 +294,9 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
    used by the operator class.  This lets the core code look up operators for
    temporal constraint indexes.
+   The optional thirteenth method <function>without_portion</function> is used by
+   <literal>RESTRICT</literal> foreign keys to compute the portion of history
+   that was lost.
  </para>
 
  <variablelist>
@@ -1235,6 +1238,108 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+       This is used by temporal foreign keys to compute the part
+       of history that was lost by an update.
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15e0..85ef539d07c 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
+       <entry>computes remaining duration(s) after deleting
+       second parameter from first (optional)</entry>
index 0901543a60a..38740b46bbf 100644
@@ -143,13 +143,17 @@ gistvalidate(Oid opclassoid)
@@ -271,7 +275,7 @@ gistvalidate(Oid opclassoid)
+			i == GIST_STRATNUM_PROC || i == GIST_WITHOUT_PORTION_PROC)
@@ -344,6 +348,7 @@ gistadjustmembers(Oid opfamilyoid,

 	va_start(ap, maxargs);
 
 /*
 
+/*

+/*
 
 Datum
 
index 22dd04c1418..8d6c99f5ebe 100644
+#define GIST_WITHOUT_PORTION_PROC		13
+#define GISTNProcs					13
 
 /*
index 5d7fe292bf6..0a4db325faa 100644
@@ -609,6 +609,9 @@
@@ -649,6 +652,9 @@
 
 # gin
index cbbe8acd382..4139df94c9b 100644
@@ -10739,6 +10739,10 @@
@@ -11026,6 +11030,10 @@

+-- without_portion

+-- without_portion

From 7fb98fb66986e168ccfa36cd41a000b98ca7b5e4 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Nov 2024 14:05:48 -0800
Subject: [PATCH v45 3/8] Fix NOACTION temporal foreign keys when the
 referenced endpoints change

If a referenced UPDATE changes the temporal start/end times, shrinking
the span the row is valid, we get a false return from ri_Check_Pk_Match,
but overlapping references may still be valid, if their reference didn't
overlap with the removed span.

We need to consider what span(s) are still provided in the referenced
table. Instead of returning that from ri_Check_Pk_Match, we can just
we look it up in the main SQL query.

Reported-by: Sam Gabrielsson
Author: Paul Jungwirth
---
 src/backend/catalog/pg_constraint.c           | 16 ++-
 src/backend/commands/tablecmds.c              |  4 +-
 src/backend/utils/adt/ri_triggers.c           | 98 ++++++++++++++++++-
 src/include/catalog/pg_constraint.h           |  3 +-
 src/include/catalog/pg_operator.dat           |  6 +-
 .../regress/expected/without_overlaps.out     | 68 ++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 60 +++++++++++-
 7 files changed, 244 insertions(+), 11 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..fe754ecd1d8 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1609,11 +1609,14 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
  * aggedcontainedbyoperoid is also a ContainedBy operator,
  * but one whose rhs is a multirange.
  * 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.
  */
 void
 FindFKPeriodOpers(Oid opclass,
 				  Oid *containedbyoperoid,
-				  Oid *aggedcontainedbyoperoid)
+				  Oid *aggedcontainedbyoperoid,
+				  Oid *intersectoperoid)
 {
 	Oid			opfamily = InvalidOid;
 	Oid			opcintype = InvalidOid;
@@ -1654,6 +1657,17 @@ FindFKPeriodOpers(Oid opclass,
 									 ANYMULTIRANGEOID,
 									 aggedcontainedbyoperoid,
 									 &strat);
+
+	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);
+	}
 }
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1af2e2bffb2..1f8e381bc0a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10224,8 +10224,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
+		Oid			intersectoperoid;
 
-		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid);
+		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid,
+						  &intersectoperoid);
 	}
 
 	/* First, create the constraint catalog entry itself. */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 91792cb2a47..d6b5e43134d 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -129,6 +129,7 @@ typedef struct RI_ConstraintInfo
 	Oid			ff_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (FK = FK) */
 	Oid			period_contained_by_oper;	/* anyrange <@ anyrange */
 	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
+	Oid			period_intersect_oper;	/* anyrange * anyrange */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -731,8 +732,11 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	 * not do anything.  However, this check should only be made in the NO
 	 * ACTION case; in RESTRICT cases we don't wish to allow another row to be
 	 * substituted.
+	 *
+	 * If the foreign key has PERIOD, we incorporate looking for replacement
+	 * rows in the main SQL query below, so we needn't do it here.
 	 */
-	if (is_no_action &&
+	if (is_no_action && !riinfo->hasperiod &&
 		ri_Check_Pk_Match(pk_rel, fk_rel, oldslot, riinfo))
 	{
 		table_close(fk_rel, RowShareLock);
@@ -750,8 +754,10 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL)
 	{
 		StringInfoData querybuf;
+		char		pkrelname[MAX_QUOTED_REL_NAME_LEN];
 		char		fkrelname[MAX_QUOTED_REL_NAME_LEN];
 		char		attname[MAX_QUOTED_NAME_LEN];
+		char		periodattname[MAX_QUOTED_NAME_LEN];
 		char		paramname[16];
 		const char *querysep;
 		Oid			queryoids[RI_MAX_NUMKEYS];
@@ -763,6 +769,26 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 		 *		   FOR KEY SHARE OF x
 		 * The type id's for the $ parameters are those of the
 		 * corresponding PK attributes.
+		 *
+		 * For temporal foreign keys a reference could still be valid,
+		 * if the referenced range didn't change too much.
+		 * Also if the referencing time span extends past the current PK row,
+		 * we don't want to check that part: some other PK row should fulfill it.
+		 * We only want to validate the part matching the PK record we've changed.
+		 * Therefore to find invalid records we do this:
+		 * SELECT 1
+		 * FROM [ONLY] <fktable> x
+		 * WHERE $1 = x.fkatt1 [AND ...] AND $n && x.fkperiod
+		 * AND NOT coalesce((x.fkperiod * $n) <@
+		 *  (SELECT range_agg(r)
+		 *   FROM (SELECT y.pkperiod r
+		 *         FROM [ONLY] <pktable> y
+		 *         WHERE $1 = y.pkatt1 [AND ...] AND $n && y.pkperiod
+		 *         FOR KEY SHARE OF y) y2), false)
+		 * FOR KEY SHARE OF x
+		 * We need the coalesce in case the first subquery returns no rows.
+		 * We need the second subquery because FOR KEY SHARE doesn't support
+		 * aggregate queries.
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
@@ -787,6 +813,73 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 			querysep = "AND";
 			queryoids[i] = pk_type;
 		}
+
+		/*
+		 * Don't fail if the remaining history still fulfills the references.
+		 * Only check the part of the references intersecting with oldslot,
+		 * since the rest would be fulfilled by some other pk record.
+		 *
+		 * For NOACTION we can query the pk table and use whatever we find
+		 * (instead of calling ri_Check_Pk_Match above).
+		 */
+		if (riinfo->hasperiod && is_no_action)
+		{
+			Oid				pk_period_type = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
+			Oid				fk_period_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+			StringInfoData	intersectbuf;
+			StringInfoData	replacementsbuf;
+			char *pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
+				"" : "ONLY ";
+
+			quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+			sprintf(paramname, "$%d", riinfo->nkeys);
+
+			appendStringInfoString(&querybuf, " AND NOT coalesce(");
+
+			/* Intersect the fk with the old pk range */
+			initStringInfo(&intersectbuf);
+			appendStringInfoString(&intersectbuf, "(");
+			ri_GenerateQual(&intersectbuf, "",
+							attname, fk_period_type,
+							riinfo->period_intersect_oper,
+							paramname, pk_period_type);
+			appendStringInfoString(&intersectbuf, ")");
+
+			/* Find the remaining history */
+			initStringInfo(&replacementsbuf);
+			appendStringInfoString(&replacementsbuf, "(SELECT pg_catalog.range_agg(r) FROM ");
+
+			quoteOneName(periodattname, RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+			quoteRelationName(pkrelname, pk_rel);
+			appendStringInfo(&replacementsbuf, "(SELECT y.%s r FROM %s%s y",
+							 periodattname, pk_only, pkrelname);
+
+			/* Restrict pk rows to what matches */
+			querysep = "WHERE";
+			for (int i = 0; i < riinfo->nkeys; i++)
+			{
+				Oid		pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]);
+
+				quoteOneName(attname,
+							 RIAttName(pk_rel, riinfo->pk_attnums[i]));
+				sprintf(paramname, "$%d", i + 1);
+				ri_GenerateQual(&replacementsbuf, querysep,
+								paramname, pk_type,
+								riinfo->pp_eq_oprs[i],
+								attname, pk_type);
+				querysep = "AND";
+				queryoids[i] = pk_type;
+			}
+			appendStringInfoString(&replacementsbuf, " FOR KEY SHARE OF y) y2)");
+
+			ri_GenerateQual(&querybuf, "",
+							intersectbuf.data, fk_period_type,
+							riinfo->agged_period_contained_by_oper,
+							replacementsbuf.data, ANYMULTIRANGEOID);
+			/* end of coalesce: */
+			appendStringInfoString(&querybuf, ", false)");
+		}
+
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
 
 		/* Prepare and save the plan */
@@ -2244,7 +2337,8 @@ ri_LoadConstraintInfo(Oid constraintOid)
 
 		FindFKPeriodOpers(opclass,
 						  &riinfo->period_contained_by_oper,
-						  &riinfo->agged_period_contained_by_oper);
+						  &riinfo->agged_period_contained_by_oper,
+						  &riinfo->period_intersect_oper);
 	}
 
 	ReleaseSysCache(tup);
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..5edd4eb8a33 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -288,7 +288,8 @@ extern void DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 									   int *num_fk_del_set_cols, AttrNumber *fk_del_set_cols);
 extern void FindFKPeriodOpers(Oid opclass,
 							  Oid *containedbyoperoid,
-							  Oid *aggedcontainedbyoperoid);
+							  Oid *aggedcontainedbyoperoid,
+							  Oid *intersectoperoid);
 
 extern bool check_functional_grouping(Oid relid,
 									  Index varno, Index varlevelsup,
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 0e7511dde1c..aeddd292e2c 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3150,7 +3150,8 @@
 { oid => '3899', descr => 'range difference',
   oprname => '-', oprleft => 'anyrange', oprright => 'anyrange',
   oprresult => 'anyrange', oprcode => 'range_minus' },
-{ oid => '3900', descr => 'range intersection',
+{ oid => '3900', oid_symbol => 'OID_RANGE_INTERSECT_RANGE_OP',
+  descr => 'range intersection',
   oprname => '*', oprleft => 'anyrange', oprright => 'anyrange',
   oprresult => 'anyrange', oprcom => '*(anyrange,anyrange)',
   oprcode => 'range_intersect' },
@@ -3417,7 +3418,8 @@
 { oid => '4393', descr => 'multirange minus',
   oprname => '-', oprleft => 'anymultirange', oprright => 'anymultirange',
   oprresult => 'anymultirange', oprcode => 'multirange_minus' },
-{ oid => '4394', descr => 'multirange intersect',
+{ oid => '4394', oid_symbol => 'OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP',
+  descr => 'multirange intersect',
   oprname => '*', oprleft => 'anymultirange', oprright => 'anymultirange',
   oprresult => 'anymultirange', oprcom => '*(anymultirange,anymultirange)',
   oprcode => 'multirange_intersect' },
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 8b5ecab6fd8..b2744a49a99 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -1690,7 +1690,8 @@ DELETE FROM temporal_rng WHERE id = '[5,6)';
 INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[5,6)', daterange('2018-01-01', '2018-02-01')),
   ('[5,6)', daterange('2018-02-01', '2018-03-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
 UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
 WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- A PK update sliding the edge between two referenced rows:
@@ -1703,6 +1704,37 @@ UPDATE temporal_rng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
                     WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-01', '2018-03-01')),
+  ('[1,2)', daterange('2018-03-01', '2018-06-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2rng".
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2rng".
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[2,3)', daterange('2018-01-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[5,6)', daterange('2018-01-15', '2018-02-01'), '[2,3)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15')
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced:
 UPDATE temporal_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');
@@ -2178,7 +2210,8 @@ DELETE FROM temporal_mltrng WHERE id = '[5,6)';
 INSERT INTO temporal_mltrng (id, valid_at) VALUES
   ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
   ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
-INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
 -- A PK update sliding the edge between two referenced rows:
@@ -2191,6 +2224,37 @@ UPDATE temporal_mltrng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
                     WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced multirange but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))),
+  ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'),
+  ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced multirange is fine:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), {[2018-01-01,2018-03-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- a PK update changing only the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), {[2018-01-01,2018-03-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15'))
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced:
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index ce58171bc35..8f472867e28 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -1235,7 +1235,8 @@ DELETE FROM temporal_rng WHERE id = '[5,6)';
 INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[5,6)', daterange('2018-01-01', '2018-02-01')),
   ('[5,6)', daterange('2018-02-01', '2018-03-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
 UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
 WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- A PK update sliding the edge between two referenced rows:
@@ -1248,6 +1249,33 @@ UPDATE temporal_rng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
                     WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-01', '2018-03-01')),
+  ('[1,2)', daterange('2018-03-01', '2018-06-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[2,3)', daterange('2018-01-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[5,6)', daterange('2018-01-15', '2018-02-01'), '[2,3)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15')
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced:
 UPDATE temporal_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');
@@ -1692,7 +1720,8 @@ DELETE FROM temporal_mltrng WHERE id = '[5,6)';
 INSERT INTO temporal_mltrng (id, valid_at) VALUES
   ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
   ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
-INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
 -- A PK update sliding the edge between two referenced rows:
@@ -1705,6 +1734,33 @@ UPDATE temporal_mltrng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
                     WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced multirange but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))),
+  ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'),
+  ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced multirange is fine:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[5,6)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15'))
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced:
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
-- 
2.42.0

From 5204dbe5648ee9f922354a3f50a328652fca06a9 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 12 Nov 2024 09:19:44 -0800
Subject: [PATCH v45 4/8] Fix RESTRICT temporal foreign keys when the
 referenced endpoints change

A RESTICT foreign key must fail if the referenced key changes. A
temporal RESTRICT foreign key should fail if the key changes for a span
that is referenced. Changing the key for an unreferenced span is okay,
even if that row is referenced during some portion of its span that
didn't change. Therefore we have to compute which part(s) were lost, and
fail if we find any references overlapping those parts. We can still use
the same SQL as normal, because we need to adjust the PERIOD parameter
we search for. We can call without_portion to get the lost parts: they
are oldslot.pkperiod - newslot.pkperiod. This commit adds code for that
to ri_restrict, along with tests.

Author: Paul Jungwirth
---
 src/backend/catalog/pg_constraint.c           |  25 ++-
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/utils/adt/ri_triggers.c           | 156 ++++++++++++++++--
 src/include/catalog/pg_constraint.h           |   9 +-
 .../regress/expected/without_overlaps.out     |  68 +++++++-
 src/test/regress/sql/without_overlaps.sql     |  60 ++++++-
 6 files changed, 290 insertions(+), 34 deletions(-)

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index fe754ecd1d8..4681019b9f9 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1600,9 +1600,9 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 }
 
 /*
- * FindFkPeriodOpers -
+ * FindFkPeriodOpersAndProcs -
  *
- * Looks up the operator oids used for the PERIOD part of a temporal foreign key.
+ * Looks up the operator and procedure oids used for the PERIOD part of a temporal foreign key.
  * The opclass should be the opclass of that PERIOD element.
  * Everything else is an output: containedbyoperoid is the ContainedBy operator for
  * types matching the PERIOD element.
@@ -1611,12 +1611,16 @@ 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.
+ * withoutportionoid is a set-returning function computing
+ * the difference between one range and another,
+ * returning each result range in a separate row.
  */
 void
-FindFKPeriodOpers(Oid opclass,
-				  Oid *containedbyoperoid,
-				  Oid *aggedcontainedbyoperoid,
-				  Oid *intersectoperoid)
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *containedbyoperoid,
+						  Oid *aggedcontainedbyoperoid,
+						  Oid *intersectoperoid,
+						  Oid *withoutportionoid)
 {
 	Oid			opfamily = InvalidOid;
 	Oid			opcintype = InvalidOid;
@@ -1668,6 +1672,15 @@ FindFKPeriodOpers(Oid opclass,
 		default:
 			elog(ERROR, "Unexpected opcintype: %u", opcintype);
 	}
+
+	*withoutportionoid = InvalidOid;
+	*withoutportionoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_WITHOUT_PORTION_PROC);
+	if (!OidIsValid(*withoutportionoid))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("could not identify a without_overlaps support function for type %s", format_type_be(opcintype)),
+				errhint("Define a without_overlaps support function for operator class \"%d\" for access method \"%s\".",
+						opclass, "gist"));
 }
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1f8e381bc0a..d20cb0f9e6e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10225,9 +10225,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			withoutoverlapsoid;
 
-		FindFKPeriodOpers(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid,
-						  &intersectoperoid);
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid,
+								  &aggedperiodoperoid, &intersectoperoid,
+								  &withoutoverlapsoid);
 	}
 
 	/* First, create the constraint catalog entry itself. */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index d6b5e43134d..002bd02582a 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -130,6 +130,7 @@ typedef struct RI_ConstraintInfo
 	Oid			period_contained_by_oper;	/* anyrange <@ anyrange */
 	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
 	Oid			period_intersect_oper;	/* anyrange * anyrange */
+	Oid			without_portion_proc;	/* anyrange - anyrange SRF */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -207,7 +208,7 @@ static void ri_BuildQueryKey(RI_QueryKey *key,
 							 const RI_ConstraintInfo *riinfo,
 							 int32 constr_queryno);
 static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
-						 const RI_ConstraintInfo *riinfo, bool rel_is_pk);
+						 const RI_ConstraintInfo *riinfo, bool rel_is_pk, bool skip_period);
 static bool ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid,
 							   Datum lhs, Datum rhs);
 
@@ -229,6 +230,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 detectNewRows, int expect_OK);
 static void ri_ExtractValues(Relation rel, TupleTableSlot *slot,
 							 const RI_ConstraintInfo *riinfo, bool rel_is_pk,
@@ -450,6 +452,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, (Datum) 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -614,6 +617,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,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -711,8 +715,18 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	Relation	fk_rel;
 	Relation	pk_rel;
 	TupleTableSlot *oldslot;
+	TupleTableSlot *newslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	AttrNumber	pkperiodattno = InvalidAttrNumber;
+	AttrNumber	fkperiodattno = InvalidAttrNumber;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
+	FmgrInfo	flinfo;
+	ReturnSetInfo	rsi;
+	LOCAL_FCINFO(fcinfo, 2);
+	bool		multiplelost = false;
+	bool		finished = false;
 
 	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
 									trigdata->tg_relation, true);
@@ -726,6 +740,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	fk_rel = table_open(riinfo->fk_relid, RowShareLock);
 	pk_rel = trigdata->tg_relation;
 	oldslot = trigdata->tg_trigslot;
+	newslot = trigdata->tg_newslot;
 
 	/*
 	 * If another PK row now exists providing the old key values, we should
@@ -743,6 +758,12 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 		return PointerGetDatum(NULL);
 	}
 
+	if (riinfo->hasperiod)
+	{
+		pkperiodattno = riinfo->pk_attnums[riinfo->nkeys - 1];
+		fkperiodattno = riinfo->fk_attnums[riinfo->nkeys - 1];
+	}
+
 	SPI_connect();
 
 	/*
@@ -789,6 +810,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 		 * We need the coalesce in case the first subquery returns no rows.
 		 * We need the second subquery because FOR KEY SHARE doesn't support
 		 * aggregate queries.
+		 *
+		 * For RESTRICT keys we can't query pktable, so instead we use the old
+		 * and new periods to see what was removed, and look for references
+		 * matching that. If the scalar key part changed, then this is
+		 * (where $n is the old period and $2n the new):
+		 *   $n && x.fkperiod
+		 * But if the scalar key part didn't change, then we only lost part of
+		 * the time span, so we should look for:
+		 *   (SELECT range_agg(r) FROM without_portion($n, $2n) wo(r))
+		 *     && x.fkperiod
 		 * ----------
 		 */
 		initStringInfo(&querybuf);
@@ -824,14 +855,14 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 		 */
 		if (riinfo->hasperiod && is_no_action)
 		{
-			Oid				pk_period_type = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]);
-			Oid				fk_period_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+			Oid				pk_period_type = RIAttType(pk_rel, pkperiodattno);
+			Oid				fk_period_type = RIAttType(fk_rel, fkperiodattno);
 			StringInfoData	intersectbuf;
 			StringInfoData	replacementsbuf;
 			char *pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
 				"" : "ONLY ";
 
-			quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]));
+			quoteOneName(attname, RIAttName(fk_rel, fkperiodattno));
 			sprintf(paramname, "$%d", riinfo->nkeys);
 
 			appendStringInfoString(&querybuf, " AND NOT coalesce(");
@@ -849,7 +880,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 			initStringInfo(&replacementsbuf);
 			appendStringInfoString(&replacementsbuf, "(SELECT pg_catalog.range_agg(r) FROM ");
 
-			quoteOneName(periodattname, RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+			quoteOneName(periodattname, RIAttName(pk_rel, pkperiodattno));
 			quoteRelationName(pkrelname, pk_rel);
 			appendStringInfo(&replacementsbuf, "(SELECT y.%s r FROM %s%s y",
 							 periodattname, pk_only, pkrelname);
@@ -889,12 +920,85 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
 	/*
 	 * We have a plan now. Run it to check for existing references.
+	 *
+	 * Normally we only loop once here. But if
+	 * we have a RESTRICT constraint with a PERIOD,
+	 * we must only consider the timespan that was lost.
+	 *
+	 * If the scalar key part was UPDATEd,
+	 * then all of oldslot.pkperiod was lost
+	 * (whether the endpoints changed or not).
+	 * That's what we already check by default.
+	 *
+	 * Otherwise only oldslot.pkperiod - newslot.pkperiod was lost.
+	 * That may be more than one range, so we use the
+	 * without_portion set-returning function and loop
+	 * over its results. It also may be empty,
+	 * meaning nothing was lost, and no check is required.
+	 * We shouldn't be here if neither the scalar nor PERIOD part changed,
+	 * but it's easy to support anyway.
+	 *
+	 * For a DELETE, oldslot.pkperiod was lost,
+	 * which is what we check for by default.
 	 */
-	ri_PerformCheck(riinfo, &qkey, qplan,
-					fk_rel, pk_rel,
-					oldslot, NULL,
-					true,		/* must detect new rows */
-					SPI_OK_SELECT);
+	if (riinfo->hasperiod && !is_no_action)
+	{
+		if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)
+			&& ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, true))
+		{
+			multiplelost = true;
+
+			fmgr_info(riinfo->without_portion_proc, &flinfo);
+			rsi.type = T_ReturnSetInfo;
+			rsi.econtext = CreateStandaloneExprContext();
+			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 = slot_getattr(oldslot, pkperiodattno,
+												 &fcinfo->args[0].isnull);
+			fcinfo->args[1].value = slot_getattr(newslot, pkperiodattno,
+												 &fcinfo->args[1].isnull);
+
+			targetRangeParam = riinfo->nkeys;
+		}
+	}
+
+	while (!finished)
+	{
+		if (multiplelost)
+		{
+			/* Compute a span that was actually lost. */
+			targetRange = FunctionCallInvoke(fcinfo);
+
+			/*
+			 * If we have no more lost spans to check, we're done.
+			 * If no span was lost, we don't even need to check the foreign key.
+			 */
+			if (rsi.isDone == ExprEndResult)
+				break;
+
+			if (fcinfo->isnull)
+				elog(ERROR, "Get a null from without_portion function");
+		}
+		else
+			finished = true;
+
+		ri_PerformCheck(riinfo, &qkey, qplan,
+						fk_rel, pk_rel,
+						oldslot, NULL,
+						targetRangeParam, targetRange,
+						true,		/* must detect new rows */
+						SPI_OK_SELECT);
+
+	}
+
+	/* Free this before we shut down SPI since our memctx is a child */
+	if (multiplelost)
+		FreeExprContext(rsi.econtext, false);
 
 	if (SPI_finish() != SPI_OK_FINISH)
 		elog(ERROR, "SPI_finish failed");
@@ -994,6 +1098,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1110,6 +1215,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, (Datum) 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1337,6 +1443,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,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1394,7 +1501,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
 		return false;
 
 	/* If all old and new key values are equal, no check is needed */
-	if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true))
+	if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, false))
 		return false;
 
 	/* Else we need to fire the trigger. */
@@ -1487,7 +1594,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
 		return true;
 
 	/* If all old and new key values are equal, no check is needed */
-	if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false))
+	if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false, false))
 		return false;
 
 	/* Else we need to fire the trigger. */
@@ -2335,10 +2442,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->without_portion_proc);
 	}
 
 	ReleaseSysCache(tup);
@@ -2481,6 +2589,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 detectNewRows, int expect_OK)
 {
 	Relation	query_rel,
@@ -2536,6 +2645,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
@@ -2956,6 +3071,9 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
  * For normal FKs we check for equality.
  * For temporal FKs we check that the PK side is a superset of its old value,
  * or the FK side is a subset of its old value.
+ * If skip_period is set, we ignore the last key element.
+ * This lets us ask if the scalar key parts changed,
+ * ignoring the PERIOD.
  *
  * Note: at some point we might wish to redefine this as checking for
  * "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be
@@ -2964,17 +3082,19 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
  */
 static bool
 ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
-			 const RI_ConstraintInfo *riinfo, bool rel_is_pk)
+			 const RI_ConstraintInfo *riinfo, bool rel_is_pk, bool skip_period)
 {
 	const int16 *attnums;
 
+	Assert(skip_period ? riinfo->hasperiod : true);
+
 	if (rel_is_pk)
 		attnums = riinfo->pk_attnums;
 	else
 		attnums = riinfo->fk_attnums;
 
 	/* XXX: could be worthwhile to fetch all necessary attrs at once */
-	for (int i = 0; i < riinfo->nkeys; i++)
+	for (int i = 0; i < riinfo->nkeys - (skip_period ? 1 : 0); i++)
 	{
 		Datum		oldvalue;
 		Datum		newvalue;
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 5edd4eb8a33..5f25c5a0065 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -286,10 +286,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 *withoutportionoid);
 
 extern bool check_functional_grouping(Oid relid,
 									  Index varno, Index varlevelsup,
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index b2744a49a99..799b6770579 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -1778,7 +1778,8 @@ DELETE FROM temporal_rng WHERE id = '[5,6)';
 INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[5,6)', daterange('2018-01-01', '2018-02-01')),
   ('[5,6)', daterange('2018-02-01', '2018-03-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
 UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
 WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- A PK update sliding the edge between two referenced rows:
@@ -1793,6 +1794,37 @@ SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-
 WHERE id = '[6,7)';
 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)=([6,7), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng".
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-01', '2018-03-01')),
+  ('[1,2)', daterange('2018-03-01', '2018-06-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2rng".
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2rng".
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[2,3)', daterange('2018-01-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-15', '2018-02-01'), '[2,3)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15')
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced (even before commit):
 BEGIN;
   ALTER TABLE temporal_fk_rng2rng
@@ -2294,7 +2326,8 @@ DELETE FROM temporal_mltrng WHERE id = '[5,6)';
 INSERT INTO temporal_mltrng (id, valid_at) VALUES
   ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
   ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
-INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
 -- A PK update sliding the edge between two referenced rows:
@@ -2309,6 +2342,37 @@ SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(date
 WHERE id = '[6,7)';
 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)=([6,7), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- a PK update shrinking the referenced multirange but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))),
+  ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'),
+  ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced multirange is fine:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), {[2018-01-01,2018-03-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- a PK update changing only the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+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)=([1,2), {[2018-01-01,2018-03-01)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15'))
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced (even before commit):
 BEGIN;
   ALTER TABLE temporal_fk_mltrng2mltrng
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
index 8f472867e28..f7de3871093 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -1316,7 +1316,8 @@ DELETE FROM temporal_rng WHERE id = '[5,6)';
 INSERT INTO temporal_rng (id, valid_at) VALUES
   ('[5,6)', daterange('2018-01-01', '2018-02-01')),
   ('[5,6)', daterange('2018-02-01', '2018-03-01'));
-INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)');
 UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01')
 WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01');
 -- A PK update sliding the edge between two referenced rows:
@@ -1329,6 +1330,33 @@ UPDATE temporal_rng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
                     WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', daterange('2018-01-01', '2018-03-01')),
+  ('[1,2)', daterange('2018-03-01', '2018-06-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'),
+  ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01')
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[2,3)', daterange('2018-01-01', '2018-03-01'));
+INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES
+  ('[3,4)', daterange('2018-01-15', '2018-02-01'), '[2,3)');
+UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15')
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced (even before commit):
 BEGIN;
   ALTER TABLE temporal_fk_rng2rng
@@ -1797,7 +1825,8 @@ DELETE FROM temporal_mltrng WHERE id = '[5,6)';
 INSERT INTO temporal_mltrng (id, valid_at) VALUES
   ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))),
   ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01')));
-INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)');
 UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
 WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01'));
 -- A PK update sliding the edge between two referenced rows:
@@ -1810,6 +1839,33 @@ UPDATE temporal_mltrng
 SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05'))
                     WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END
 WHERE id = '[6,7)';
+-- a PK update shrinking the referenced multirange but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))),
+  ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'),
+  ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced multirange is fine:
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01'))
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole multirange:
+UPDATE temporal_mltrng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_mltrng (id, valid_at) VALUES
+  ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES
+  ('[3,4)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15'))
+WHERE id = '[2,3)';
 -- a PK update that fails because both are referenced (even before commit):
 BEGIN;
   ALTER TABLE temporal_fk_mltrng2mltrng
-- 
2.42.0

From 2963a261d55400f3ebb00264cce7c8d7729cffc4 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v45 5/8] Add intersect support func for FOR PORTION OF

  These just call the existing intersect functions,
  but they let us compute the portion of a row that is updated/deleted
  in a FOR PORTION command.
---
 doc/src/sgml/gist.sgml                 | 57 ++++++++++++++++++++++++--
 doc/src/sgml/xindex.sgml               |  8 +++-
 src/backend/access/gist/gistvalidate.c |  8 +++-
 src/include/access/gist.h              |  3 +-
 src/include/catalog/pg_amproc.dat      |  9 ++++
 5 files changed, 79 insertions(+), 6 deletions(-)

index 1fbddf1bfb0..f3314eccb9e 100644
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and eight that are optional.
+   <acronym>GiST</acronym> must provide, and nine that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -296,7 +296,11 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    temporal constraint indexes.
    The optional thirteenth method <function>without_portion</function> is used by
    <literal>RESTRICT</literal> foreign keys to compute the portion of history
-   that was lost.
+   that was lost and also by <literal>FOR PORTION OF</literal> to compute the
+   leftover records outside the targeted bounds.
+   The optional fourteenth method <function>intersect</function> is used by
+   <literal>FOR PORTION OF</literal> to compute the new bounds of the updated/
+   deleted record.
  </para>
 
  <variablelist>
@@ -1247,7 +1251,8 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
       <para>
        This is used by temporal foreign keys to compute the part
-       of history that was lost by an update.
+       of history that was lost by an update and also by temporal update/delete
+       commands to compute the bounds of the untouched duration.
       </para>
 
       <para>
@@ -1336,6 +1341,52 @@ my_range_without_portion(PG_FUNCTION_ARGS)
         /* do when there is no more left */
         SRF_RETURN_DONE(funcctx);
 }
+</programlisting>
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
index 85ef539d07c..d2fac08d742 100644
 
   <para>
-   GiST indexes have thirteen support functions, eight of which are optional,
+   GiST indexes have fourteen support functions, nine of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -602,6 +602,12 @@
        second parameter from first (optional)</entry>
        <entry>13</entry>
       </row>
+      <row>
+       <entry><function>intersect</function></entry>
+       <entry>computes intersection with <literal>FOR PORTION OF</literal>
+        bounds (optional)</entry>
+       <entry>14</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 38740b46bbf..5dcad45eef5 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -154,6 +154,10 @@ gistvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, opcintype, true, true,
 											2, 2, opcintype, opcintype);
 				break;
+			case GIST_INTERSECT_PROC:
+				ok = check_amproc_signature(procform->amproc, InvalidOid, false, true,
+											2, 2, opcintype, opcintype);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -275,7 +279,8 @@ gistvalidate(Oid opclassoid)
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
 			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
-			i == GIST_STRATNUM_PROC || i == GIST_WITHOUT_PORTION_PROC)
+			i == GIST_STRATNUM_PROC || i == GIST_WITHOUT_PORTION_PROC ||
+			i == GIST_INTERSECT_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -349,6 +354,7 @@ gistadjustmembers(Oid opfamilyoid,
 			case GIST_SORTSUPPORT_PROC:
 			case GIST_STRATNUM_PROC:
 			case GIST_WITHOUT_PORTION_PROC:
+			case GIST_INTERSECT_PROC:
 				/* Optional, so force it to be a soft family dependency */
 				op->ref_is_hard = false;
 				op->ref_is_family = true;
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index 8d6c99f5ebe..d39b13ad932 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -41,7 +41,8 @@
 #define GIST_SORTSUPPORT_PROC			11
 #define GIST_STRATNUM_PROC				12
 #define GIST_WITHOUT_PORTION_PROC		13
-#define GISTNProcs					13
+#define GIST_INTERSECT_PROC				14
+#define GISTNProcs					14
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 0a4db325faa..4e5c2bbeef8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -509,6 +509,9 @@
 { amprocfamily => 'gist/box_ops', amproclefttype => 'box',
   amprocrighttype => 'box', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/box_ops', amproclefttype => 'box',
+  amprocrighttype => 'box', amprocnum => '14',
+  amproc => 'box_intersect(box,box)' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '1',
   amproc => 'gist_poly_consistent' },
@@ -612,6 +615,9 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '13',
   amproc => 'range_without_portion(anyrange,anyrange)' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '14',
+  amproc => 'range_intersect(anyrange,anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -655,6 +661,9 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '13',
   amproc => 'multirange_without_portion(anymultirange,anymultirange)' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '14',
+  amproc => 'multirange_intersect(anymultirange,anymultirange)' },
 
 # gin

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

 src/backend/catalog/pg_constraint.c           |  14 +
 src/backend/commands/tablecmds.c              |   4 +-
 src/backend/commands/trigger.c                |  47 ++
 src/backend/executor/nodeModifyTable.c        | 498 +++++++++++-
 src/backend/parser/analyze.c                  | 228 +++++-
 src/backend/utils/adt/ri_triggers.c           |  83 +-
 src/backend/utils/cache/lsyscache.c           |  27 +
 src/include/catalog/pg_constraint.h           |   1 +
 src/include/nodes/execnodes.h                 |  24 +
 .../regress/expected/without_overlaps.out     | 303 ++++++-
 src/test/regress/sql/without_overlaps.sql     | 156 +++-
 52 files changed, 3423 insertions(+), 92 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";

 

 		dictOid = GetSysCacheOid2(TSDICTNAMENSP, Anum_pg_ts_dict_oid,
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index f8e217d6610..054e908e427 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -343,6 +343,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
    for each row inserted, updated, or deleted.
   </para>
 
+  <para>
 
+  <para>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>
 
+   <para>

diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 4681019b9f9..c474c08b868 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1620,6 +1620,7 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 						  Oid *containedbyoperoid,
 						  Oid *aggedcontainedbyoperoid,
 						  Oid *intersectoperoid,
+						  Oid *intersectprocoid,
 						  Oid *withoutportionoid)
 {
 	Oid			opfamily = InvalidOid;
@@ -1673,6 +1674,19 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 			elog(ERROR, "Unexpected opcintype: %u", opcintype);
 	}
 
+	/*
+	 * Look up the intersect proc. We use this for FOR PORTION OF
+	 * (both the operation itself and when checking foreign keys).
+	 * If this is missing we don't need to complain here,
+	 * because FOR PORTION OF will not be allowed.
+	 */
+	*intersectprocoid = InvalidOid;
+	*intersectprocoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_INTERSECT_PROC);
+
+	/*
+	 * Look up the without_portion func. We need this for RESTRICT
+	 * foreign keys and also FOR PORTION OF.
+	 */
 	*withoutportionoid = InvalidOid;
 	*withoutportionoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_WITHOUT_PORTION_PROC);
 	if (!OidIsValid(*withoutportionoid))
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d20cb0f9e6e..9551eec30da 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10225,11 +10225,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			intersectprocoid;
 		Oid			withoutoverlapsoid;
 
 		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid,
 								  &aggedperiodoperoid, &intersectoperoid,
-								  &withoutoverlapsoid);
+								  &intersectprocoid, &withoutoverlapsoid);
 	}
 
@@ -12597,6 +12598,7 @@ validateForeignKeyConstraint(char *conname,

diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 09356e46d16..c134b8e3774 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"
@@ -2632,6 +2634,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];
@@ -2731,6 +2734,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;
@@ -2822,6 +2826,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -2885,6 +2890,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];
@@ -3020,6 +3026,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++)
@@ -3169,6 +3176,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -3635,6 +3643,7 @@ typedef struct AfterTriggerSharedData
@@ -3908,6 +3917,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);

@@ -4120,6 +4130,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
@@ -4488,6 +4499,7 @@ AfterTriggerExecute(EState *estate,

@@ -6022,6 +6034,40 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6437,6 +6483,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 
index 5ca856fd279..8af35e8b05c 100644
@@ -1257,6 +1257,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index 1161520f76b..02f17fccff9 100644
 
 #include "postgres.h"
 
@@ -59,14 +60,19 @@
 
@@ -123,6 +129,19 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
+/*
+ * FPO_QueryHashEntry
+ */
 
@@ -144,6 +163,13 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
@@ -166,6 +192,7 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,

 /*
@@ -1214,6 +1241,367 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
@@ -1366,7 +1754,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1399,6 +1788,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1779,7 +2172,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2145,6 +2542,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4640,6 +5041,99 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index 3060847b133..034e0020144 100644
@@ -2560,6 +2560,14 @@ expression_tree_walker_impl(Node *node,
@@ -2706,6 +2714,8 @@ query_tree_walker_impl(Query *query,
@@ -3590,6 +3600,19 @@ expression_tree_mutator_impl(Node *node,
@@ -3771,6 +3794,7 @@ query_tree_mutator_impl(Query *query,

index 1f78dc3d530..0ab768c6ee9 100644
@@ -2042,6 +2042,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,

index 506e0631615..909ed5d7e2a 100644
 
 #include "postgres.h"

@@ -59,10 +64,16 @@

+static Node *

 
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1304,6 +1334,173 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+		List		   *funcArgs = NIL;
+		FuncExpr	   *rangeTLEExpr;
+		targetExpr = transformExpr(pstate, targetExpr, EXPR_KIND_UPDATE_PORTION);
+		funcArgs = lappend(funcArgs, copyObject(rangeVar));
+		funcArgs = lappend(funcArgs, targetExpr);
+		rangeTLEExpr = makeFuncExpr(funcid, attr->atttypid, funcArgs,
+									InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
+		tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno, range_name, false);
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2512,6 +2709,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2529,6 +2727,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2545,7 +2747,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2555,7 +2758,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2574,7 +2777,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2627,6 +2830,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 67eb96396af..067868cc305 100644

 
@@ -12336,6 +12342,20 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
 
 using_clause:
@@ -12411,6 +12431,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->stmt_location = @$;
 					$$ = (Node *) n;
 				}
 
 set_clause_list:
@@ -13914,6 +13953,44 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+opt_alias:
+			AS ColId
+				{
@@ -14747,16 +14824,25 @@ opt_timezone:
 
+/*
@@ -17809,6 +17895,7 @@ unreserved_keyword:
@@ -18434,6 +18521,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
index 063afd4933e..848c09d8182 100644
@@ -3739,6 +3739,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
@@ -4078,6 +4102,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
index 002bd02582a..5c7739a3f2b 100644
@@ -129,7 +129,8 @@ typedef struct RI_ConstraintInfo
 	Oid			agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */
-	Oid			period_intersect_oper;	/* anyrange * anyrange */
+	Oid			period_intersect_oper;	/* anyrange * anyrange (or multirange) */
 	Oid			without_portion_proc;	/* anyrange - anyrange SRF */
@@ -239,6 +240,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
+static Datum restrict_enforced_range(const ForPortionOfState *tg_temporal,

 /*
@@ -940,11 +946,34 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	 *
 	 * For a DELETE, oldslot.pkperiod was lost,
 	 * which is what we check for by default.
+	 *
+	 * In an UPDATE t FOR PORTION OF, if the scalar key part changed,
+	 * then only newslot.pkperiod was lost. Otherwise nothing was lost.
+	 *
+	 * In a DELETE FROM t FOR PORTION OF, only newslot.pkperiod was lost.
+	 * But there is no newslot, so we have to calculate the intersection
+	 * of oldslot.pkperiod and the range targeted by FOR PORTION OF.
 	 */
 	if (riinfo->hasperiod && !is_no_action)
 	{
-		if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)
-			&& ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, true))
+		/* Don't treat leftovers of FOR PORTION OF as lost */
+		if (trigdata->tg_temporal)
+		{
+			bool	isnull;
+			targetRangeParam = riinfo->nkeys;
+			if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+			{
+				if (!ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, true))
+					targetRange = slot_getattr(newslot, pkperiodattno, &isnull);
+				else
+					/* nothing to do */
+					finished = true;
+			}
+			else
+				targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+		}
+		else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)
+				 && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, true))
 		{
 			multiplelost = true;
 
@@ -2446,6 +2475,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 								  &riinfo->period_contained_by_oper,
 								  &riinfo->agged_period_contained_by_oper,
 								  &riinfo->period_intersect_oper,
+								  &riinfo->period_intersect_proc,
 								  &riinfo->without_portion_proc);
 	}
 
@@ -3331,3 +3361,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 parent's own valid time,
+ * but if there was a FOR PORTION OF clause, then we should use that to
+ * trim down the parent's 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/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index a85dc0d891f..2b7f6734719 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2188,6 +2188,33 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
index 5f25c5a0065..d7dcb7ec655 100644
@@ -290,6 +290,7 @@ extern void FindFKPeriodOpersAndProcs(Oid opclass,
 									  Oid *containedbyoperoid,
 									  Oid *aggedcontainedbyoperoid,
 									  Oid *intersectoperoid,
+									  Oid *intersectprocoid,
 									  Oid *withoutportionoid);

 /*
index 182a6956bb0..1ed5276b7d8 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -431,6 +432,26 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -558,6 +579,9 @@ typedef struct ResultRelInfo

index 0f9462493e3..7ca0248b82d 100644

+/*
+ * ForPortionOfClause

 
+/*----------
index 20446f6f836..f7a16d3a8b6 100644
@@ -145,6 +145,7 @@ extern Oid	get_rel_relam(Oid relid);
index 75912f690c2..399809940e9 100644
@@ -1105,6 +1105,24 @@ ERROR:  null value in column "b" of relation "errtst_part_2" violates not-null c
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 799b6770579..ed9a7d79c16 100644

 
 
@@ -1755,6 +1819,33 @@ UPDATE temporal_rng SET id = '[7,8)'
@@ -1840,6 +1931,34 @@ UPDATE temporal_rng SET id = '[7,8)'
+ [3,4) | [2018-01-15,2018-02-01) | [2,3)
@@ -1876,9 +1995,35 @@ BEGIN;
@@ -1907,11 +2052,37 @@ BEGIN;
@@ -2307,6 +2478,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
@@ -2388,6 +2575,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
@@ -2420,6 +2623,19 @@ BEGIN;
@@ -2448,6 +2664,19 @@ BEGIN;
 
 # ----------
 
 -- amopmethod must match owning opfamily's opfmethod
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index d886cc088c9..e725e9ccb6d 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -756,6 +756,24 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
 SET SESSION AUTHORIZATION regress_priv_user1;
 DROP TABLE errtst;

index f7de3871093..3f26af2bcfb 100644

 -- temporal UNIQUE:
 
@@ -1291,6 +1333,18 @@ COMMIT;
@@ -1368,6 +1422,18 @@ ROLLBACK;
@@ -1403,9 +1469,22 @@ BEGIN;

@@ -1434,12 +1513,25 @@ BEGIN;

@@ -1804,6 +1896,20 @@ COMMIT;
 
@@ -1878,6 +1984,20 @@ ROLLBACK;
 
 --
 -- test FK referenced deletes NO ACTION
@@ -1909,6 +2029,17 @@ BEGIN;

 --
 -- test FK referenced deletes RESTRICT
@@ -1938,6 +2069,17 @@ BEGIN;

index bff3b5aa0e3..994cd88a232 100644
@@ -114,6 +114,12 @@ is( $stderr,
 
@@ -121,6 +127,12 @@ is( $stderr,

@@ -142,16 +154,22 @@ $node_publisher->safe_psql(

 

@@ -169,6 +187,12 @@ is( $stderr,
 
@@ -176,6 +200,12 @@ is( $stderr,

@@ -291,16 +321,22 @@ $node_publisher->safe_psql(

 

 # replicate with a primary key:
@@ -314,16 +350,22 @@ $node_publisher->safe_psql(

 

 # replicate with a unique key:
@@ -337,17 +379,23 @@ $node_publisher->safe_psql(

 

 # cleanup
 
@@ -436,16 +484,22 @@ $node_publisher->safe_psql(

 

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

 

 # cleanup
@@ -579,6 +639,12 @@ is( $stderr,
 
@@ -586,6 +652,12 @@ is( $stderr,

@@ -611,6 +683,12 @@ is( $stderr,
 
@@ -618,6 +696,12 @@ is( $stderr,

@@ -643,6 +727,12 @@ is( $stderr,
 
@@ -650,6 +740,12 @@ is( $stderr,

-- 
2.42.0

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

---
 doc/src/sgml/ref/create_table.sgml            |   14 +-
 src/backend/catalog/pg_constraint.c           |   10 +
 src/backend/commands/tablecmds.c              |   77 +-
 src/backend/utils/adt/ri_triggers.c           |  558 +++++-
 src/include/catalog/pg_proc.dat               |   22 +
 src/test/regress/expected/btree_index.out     |   18 +-
 .../regress/expected/without_overlaps.out     | 1642 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     |  921 ++++++++-
 8 files changed, 3167 insertions(+), 95 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 1a1adc5ae87..ea849c82217 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1289,7 +1289,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1304,7 +1306,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1321,7 +1326,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/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index c474c08b868..7571401d804 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1611,6 +1611,8 @@ 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.
  * withoutportionoid is a set-returning function computing
  * the difference between one range and another,
  * returning each result range in a separate row.
@@ -1695,6 +1697,14 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 				errmsg("could not identify a without_overlaps support function for type %s", format_type_be(opcintype)),
 				errhint("Define a without_overlaps support function for operator class \"%d\" for access method \"%s\".",
 						opclass, "gist"));
+	/*
+	 * If the command uses FOR PORTION OF,
+	 * we will also need an intersect support proc.
+	 * If this is missing we don't need to complain here,
+	 * because FOR PORTION OF will not be allowed.
+	 */
+	if (opfamily != InvalidOid && opcintype != InvalidOid)
+		*intersectprocoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_INTERSECT_PROC);
 }
 
 /*
index 9551eec30da..590ff2c42e0 100644
@@ -519,7 +519,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
@@ -9714,6 +9714,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9809,15 +9810,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 fkattnum, fktypoid, fkcolloid);
 
 											  fkdelsetcols, NULL, NULL);
 
@@ -9900,28 +9905,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		}
 	}
 
-	/*
@@ -10297,6 +10280,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -10307,6 +10291,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
@@ -12726,17 +12717,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12787,17 +12787,26 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 5c7739a3f2b..177988e3075 100644
 
@@ -195,6 +201,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
@@ -1503,6 +1510,535 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+					riinfo->nkeys + 1, targetRange,
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+					riinfo->nkeys * 2 + 1, targetRange,
+	 * Don't SET NULL/DEFAULT more than the PK's duration,
+	targetRange = restrict_enforced_range(trigdata->tg_temporal, riinfo, oldslot);
+					riinfo->nkeys + 1, targetRange,
@@ -2631,8 +3167,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -2667,8 +3203,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -3352,6 +3890,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 		case F_RI_FKEY_CHECK_INS:
@@ -3383,16 +3927,16 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn
  * 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 parent's own valid time,
+ * 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 parent's span further.
+ * 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];
+	Datum	pkRecordRange;
+	bool	isnull;
+	int		attno = riinfo->pk_attnums[riinfo->nkeys - 1];
 
 	pkRecordRange = slot_getattr(oldslot, attno, &isnull);
 	if (isnull)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4139df94c9b..60a8c5d7e10 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4039,6 +4039,28 @@
   prorettype => 'trigger', proargtypes => '',
   prosrc => 'RI_FKey_noaction_upd' },

 
 explain (costs off)

 explain (costs off)
index ed9a7d79c16..202e9af4962 100644
@@ -2018,12 +2018,22 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
@@ -2075,39 +2085,206 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
 -- rng2rng test ON UPDATE/DELETE options
@@ -2115,7 +2292,432 @@ ALTER TABLE temporal_fk_rng2rng
@@ -2678,6 +3280,625 @@ 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)';
@@ -2686,8 +3907,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -2700,8 +3921,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -2743,7 +3964,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -2755,7 +3976,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
@@ -2777,7 +3998,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -2789,37 +4010,162 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
@@ -2827,10 +4173,73 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
@@ -2950,32 +4359,150 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenc
@@ -2983,10 +4510,67 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
index 3f26af2bcfb..c83b755679f 100644

@@ -1479,12 +1479,11 @@ SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
 DELETE FROM temporal_rng
 
@@ -1523,41 +1522,131 @@ SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
 
 --
 -- rng2rng test ON UPDATE/DELETE options
 
+--
 
+--
 
+--
@@ -1565,6 +1654,252 @@ ALTER TABLE temporal_fk_rng2rng
 
 --
 -- test FOREIGN KEY, multirange references multirange
@@ -2081,6 +2416,406 @@ 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'));
 
+--
@@ -2091,8 +2826,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -2106,8 +2841,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
 
 --
 -- partitioned FK referencing inserts
@@ -2200,36 +2935,90 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,6)' AND valid_at = daterange
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2237,11 +3026,34 @@ ALTER TABLE temporal_partitioned_fk_rng2rng

 
@@ -2364,36 +3176,90 @@ DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemu
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2401,11 +3267,34 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng

 
-- 
2.42.0

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

 

 

 /*

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 26 Nov 2024 13:18:07 +0100

On 22.11.24 01:30, Paul Jungwirth wrote:
>> get_equal_strategy_number_for_am().  That function already

I think this is the right idea, but after digging around a bit more, I 
think more could/should be done.

After these changes, the difference between 
get_equal_strategy_number_for_am() and get_equal_strategy_number() is 
kind of pointless.  We should really just use 
get_equal_strategy_number() for all purposes.

But then you have the problem that IsIndexUsableForReplicaIdentityFull() 
doesn't have the opclass IDs available in the IndexInfo structure.  You 
appear to have worked around that by writing

+   if (get_equal_strategy_number_for_am(indexInfo->ii_Am, InvalidOid) 
== InvalidStrategy)

which I suppose will have the same ultimate result as before that patch, 
but it seems kind of incomplete.

I figure this could all be simpler if 
IsIndexUsableForReplicaIdentityFull() used the index relcache entry 
directly instead of going the detour through IndexInfo.  Then we have 
all the information available, and this should ultimately all work 
properly for suitable GiST indexes as well.

I have attached three patches that show how that could be done.  (This 
would work in conjunction with your new tests.  (Although now we could 
also test GiST with replica identity full?))

The comment block for IsIndexUsableForReplicaIdentityFull() makes a 
bunch of claims that are not all explicitly supported by the code.  The 
code doesn't actually check the AM, this is all only done indirectly via 
other checks.  The second point (about tuples_equal()) appears to be 
slightly wrong, because while you need an equals operator from the type 
cache, that shouldn't prevent you from also using a different index AM 
than btree or hash for the replica identity index.  And the stuff about 
amgettuple, if that is important, why is it only checked for assert builds?
From fdf5a2335cb987953b070dec19e20a3f9caaaa5e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 26 Nov 2024 11:25:12 +0100
Subject: [PATCH 1/3] Simplify IsIndexUsableForReplicaIdentityFull()

Take Relation as argument instead of IndexInfo.  Building the
IndexInfo is an unnecessary intermediate step here.
---
 src/backend/replication/logical/relation.c | 16 +++++++---------
 src/backend/replication/logical/worker.c   |  2 +-
 src/include/replication/logicalrelation.h  |  2 +-
 3 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index f5a0ef2bd9d..1b426f43fd7 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -750,11 +750,9 @@ FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap)
 	{
 		bool		isUsableIdx;
 		Relation	idxRel;
-		IndexInfo  *idxInfo;
 
 		idxRel = index_open(idxoid, AccessShareLock);
-		idxInfo = BuildIndexInfo(idxRel);
-		isUsableIdx = IsIndexUsableForReplicaIdentityFull(idxInfo, attrmap);
+		isUsableIdx = IsIndexUsableForReplicaIdentityFull(idxRel, attrmap);
 		index_close(idxRel, AccessShareLock);
 
 		/* Return the first eligible index found */
@@ -801,22 +799,22 @@ FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap)
  * to sequential execution, which might not be a good idea in some cases.
  */
 bool
-IsIndexUsableForReplicaIdentityFull(IndexInfo *indexInfo, AttrMap *attrmap)
+IsIndexUsableForReplicaIdentityFull(Relation idxrel, AttrMap *attrmap)
 {
 	AttrNumber	keycol;
 
 	/* Ensure that the index access method has a valid equal strategy */
-	if (get_equal_strategy_number_for_am(indexInfo->ii_Am) == InvalidStrategy)
+	if (get_equal_strategy_number_for_am(idxrel->rd_rel->relam) == InvalidStrategy)
 		return false;
 
 	/* The index must not be a partial index */
-	if (indexInfo->ii_Predicate != NIL)
+	if (!heap_attisnull(idxrel->rd_indextuple, Anum_pg_index_indpred, NULL))
 		return false;
 
-	Assert(indexInfo->ii_NumIndexAttrs >= 1);
+	Assert(idxrel->rd_index->indnatts >= 1);
 
 	/* The leftmost index field must not be an expression */
-	keycol = indexInfo->ii_IndexAttrNumbers[0];
+	keycol = idxrel->rd_index->indkey.values[0];
 	if (!AttributeNumberIsValid(keycol))
 		return false;
 
@@ -834,7 +832,7 @@ IsIndexUsableForReplicaIdentityFull(IndexInfo *indexInfo, AttrMap *attrmap)
 		IndexAmRoutine *amroutine;
 
 		/* The given index access method must implement amgettuple. */
-		amroutine = GetIndexAmRoutineByAmId(indexInfo->ii_Am, false);
+		amroutine = GetIndexAmRoutineByAmId(idxrel->rd_rel->relam, false);
 		Assert(amroutine->amgettuple != NULL);
 	}
 #endif
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 925dff9cc44..46d3ad566f6 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -2931,7 +2931,7 @@ FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel,
 		/* Index must be PK, RI, or usable for REPLICA IDENTITY FULL tables */
 		Assert(GetRelationIdentityOrPK(localrel) == localidxoid ||
 			   (remoterel->replident == REPLICA_IDENTITY_FULL &&
-				IsIndexUsableForReplicaIdentityFull(BuildIndexInfo(idxrel),
+				IsIndexUsableForReplicaIdentityFull(idxrel,
 													edata->targetRel->attrmap)));
 		index_close(idxrel, AccessShareLock);
 #endif
diff --git a/src/include/replication/logicalrelation.h b/src/include/replication/logicalrelation.h
index e687b40a566..33534672ec3 100644
--- a/src/include/replication/logicalrelation.h
+++ b/src/include/replication/logicalrelation.h
@@ -48,7 +48,7 @@ extern LogicalRepRelMapEntry *logicalrep_partition_open(LogicalRepRelMapEntry *r
 														Relation partrel, AttrMap *map);
 extern void logicalrep_rel_close(LogicalRepRelMapEntry *rel,
 								 LOCKMODE lockmode);
-extern bool IsIndexUsableForReplicaIdentityFull(IndexInfo *indexInfo, AttrMap *attrmap);
+extern bool IsIndexUsableForReplicaIdentityFull(Relation idxrel, AttrMap *attrmap);
 extern Oid	GetRelationIdentityOrPK(Relation rel);
 
 #endif							/* LOGICALRELATION_H */
-- 
2.47.0

From e24695c4a912eeefe0cfc2fbfa5369a0374e6223 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 26 Nov 2024 11:47:24 +0100
Subject: [PATCH 2/3] Replace get_equal_strategy_number_for_am() by
 get_equal_strategy_number()

---
 src/backend/executor/execReplication.c     | 17 +++--------------
 src/backend/replication/logical/relation.c | 15 +++++++++++----
 src/include/executor/executor.h            |  2 +-
 3 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 54025c9f150..8313ccdb46b 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -39,7 +39,7 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
 
 /*
  * Returns the fixed strategy number, if any, of the equality operator for the
- * given index access method, otherwise, InvalidStrategy.
+ * given operator class, otherwise, InvalidStrategy.
  *
  * Currently, only Btree and Hash indexes are supported. The other index access
  * methods don't have a fixed strategy for equality operation - instead, the
@@ -47,8 +47,9 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
  * according to the operator class's definition.
  */
 StrategyNumber
-get_equal_strategy_number_for_am(Oid am)
+get_equal_strategy_number(Oid opclass)
 {
+	Oid			am = get_opclass_method(opclass);
 	int			ret;
 
 	switch (am)
@@ -68,18 +69,6 @@ get_equal_strategy_number_for_am(Oid am)
 	return ret;
 }
 
-/*
- * Return the appropriate strategy number which corresponds to the equality
- * operator.
- */
-static StrategyNumber
-get_equal_strategy_number(Oid opclass)
-{
-	Oid			am = get_opclass_method(opclass);
-
-	return get_equal_strategy_number_for_am(am);
-}
-
 /*
  * Setup a ScanKey for a search in the relation 'rel' for a tuple 'key' that
  * is setup to match 'rel' (*NOT* idxrel!).
diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index 1b426f43fd7..7cda6a792a2 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -29,6 +29,7 @@
 #include "replication/logicalrelation.h"
 #include "replication/worker_internal.h"
 #include "utils/inval.h"
+#include "utils/syscache.h"

 static MemoryContext LogicalRepRelMapContext = NULL;
@@ -784,7 +785,7 @@ FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap)
  * The reasons why only Btree and Hash indexes can be considered as usable are:
  *
  * 1) Other index access methods don't have a fixed strategy for equality
- * operation. Refer get_equal_strategy_number_for_am().
+ * operation. Refer get_equal_strategy_number().
  *
  * 2) For indexes other than PK and REPLICA IDENTITY, we need to match the
  * local and remote tuples. The equality routine tuples_equal() cannot accept
@@ -802,10 +803,9 @@ bool
 IsIndexUsableForReplicaIdentityFull(Relation idxrel, AttrMap *attrmap)
 {
 	AttrNumber	keycol;
+	oidvector  *indclass;
 
-	/* Ensure that the index access method has a valid equal strategy */
-	if (get_equal_strategy_number_for_am(idxrel->rd_rel->relam) == InvalidStrategy)
-		return false;
+	indclass = (oidvector *) DatumGetPointer(SysCacheGetAttrNotNull(INDEXRELID, idxrel->rd_indextuple, Anum_pg_index_indclass));
 
 	/* The index must not be a partial index */
 	if (!heap_attisnull(idxrel->rd_indextuple, Anum_pg_index_indpred, NULL))
@@ -813,6 +813,13 @@ IsIndexUsableForReplicaIdentityFull(Relation idxrel, AttrMap *attrmap)
 
 	Assert(idxrel->rd_index->indnatts >= 1);
 
+	/* Ensure that the index has a valid equal strategy for each column */
+	for (int i = 0; i < idxrel->rd_index->indnatts; i++)
+	{
+		if (get_equal_strategy_number(indclass->values[i]) == InvalidStrategy)
+			return false;
+	}
+
 	/* The leftmost index field must not be an expression */
 	keycol = idxrel->rd_index->indkey.values[0];
 	if (!AttributeNumberIsValid(keycol))
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 69c3ebff00a..e949cce7d98 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -658,7 +658,7 @@ extern void check_exclusion_constraint(Relation heap, Relation index,
 /*
  * prototypes from functions in execReplication.c
  */
-extern StrategyNumber get_equal_strategy_number_for_am(Oid am);
+extern StrategyNumber get_equal_strategy_number(Oid opclass);
 extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid,
 										 LockTupleMode lockmode,
 										 TupleTableSlot *searchslot,
-- 
2.47.0

From a46139648ac7c4de927315158b66c15bd2fd5360 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Tue, 26 Nov 2024 11:57:08 +0100
Subject: [PATCH 3/3] Support for GIST in get_equal_strategy_number()

---
 src/backend/executor/execReplication.c     | 9 ++++-----
 src/backend/replication/logical/relation.c | 2 +-
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 8313ccdb46b..0fa67adb990 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/genam.h"
+#include "access/gist.h"
 #include "access/relscan.h"
 #include "access/tableam.h"
 #include "access/transam.h"
@@ -40,11 +41,6 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
 /*
  * Returns the fixed strategy number, if any, of the equality operator for the
  * given operator class, otherwise, InvalidStrategy.
- *
- * Currently, only Btree and Hash indexes are supported. The other index access
- * methods don't have a fixed strategy for equality operation - instead, the
- * support routines of each operator class interpret the strategy numbers
- * according to the operator class's definition.
  */
 StrategyNumber
 get_equal_strategy_number(Oid opclass)
@@ -60,6 +56,9 @@ get_equal_strategy_number(Oid opclass)
 		case HASH_AM_OID:
 			ret = HTEqualStrategyNumber;
 			break;
+		case GIST_AM_OID:
+			ret = GistTranslateStratnum(opclass, RTEqualStrategyNumber);
+			break;
 		default:
 			/* XXX: Only Btree and Hash indexes are supported */
 			ret = InvalidStrategy;
diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index 7cda6a792a2..f686975f39d 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -782,7 +782,7 @@ FindUsableIndexForReplicaIdentityFull(Relation localrel, AttrMap *attrmap)
  * compare the tuples for non-PK/RI index scans. See
  * RelationFindReplTupleByIndex().
  *
- * The reasons why only Btree and Hash indexes can be considered as usable are:
+ * The reasons why only Btree and Hash indexes can be considered as usable are: XXX
  *
  * 1) Other index access methods don't have a fixed strategy for equality
  * operation. Refer get_equal_strategy_number().
-- 
2.47.0

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 10 Mar 2025 11:56:00 +0100

On 26.02.25 06:15, Paul Jungwirth wrote:
>  > ON DELETE RESTRICT must be specified when PERIOD BUSINESS_TIME is 
> also specified.
> 
> Here are some patches removing support for RESTRICT

I have committed this.

I think this is about as much as we can hope to get done from this patch 
series for PG18.  I don't think the subsequent patches are ready enough. 
  As an example, the FOR PORTION OF still has the problem I mentioned at 
<https://www.postgresql.org/message-id/d4c5de4d-ff2d-4ef6-b7a2-1787dfa6427b%40eisentraut.org>, 
and a few similar structural problems.  Also, I see that you have 
recently changed some things to make use of SPI, which seems 
problematic.  This needs much further analysis.

My suggestions is to close the commitfest entry as "committed" and start 
new threads and new entries for the subsequent features.

---

From: vignesh C <[email-masked]>
Date: Tue, 18 Mar 2025 15:19:51 +0530

> new threads and new entries for the subsequent features.

I have marked this commitfest entry as committed based on your
suggestion. It is better to start a new thread for the remaining work.

Regards,
Vignesh

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 19 Mar 2025 20:32:38 -0700

Here is another set of patches, just rebasing and fixing a CI failure in contrib/sepgsql.

On 3/10/25 03:56, Peter Eisentraut wrote:
 > I think this is about as much as we can hope to get done from this patch series for PG18.  I don't
 > think the subsequent patches are ready enough.  As an example, the FOR PORTION OF still has the
 > problem I mentioned at <https://www.postgresql.org/message-id/d4c5de4d-ff2d-4ef6-
 > b7a2-1787dfa6427b%40eisentraut.org>, and a few similar structural problems.

I agree that UPDATE/DELETE FOR PORTION OF is a lot to add this late in the cycle. Can you say more 
about the problems at that link though? The change you asked for has been in the patches since v45.

There is something similar happening for the range constructor, so maybe that's what you're seeing 
now? But in that case we don't have prior oid access. Looking up the function by range type name and 
arg count/type is the only way to get it. I guess I don't have to use makeFuncCall and 
transformExpr, but since I *do* need to call transformExpr on the FROM and TO inputs, it seems 
simplest to do it all together.

 > Also, I see that you
 > have recently changed some things to make use of SPI, which seems problematic.  This needs much
 > further analysis.

Okay, when you have time please let me know more about this. Using SPI fixed a lot of issues around 
firing triggers on the leftover inserts, as well as tuple routing for partitioned tables. For 
details see 
https://www.postgresql.org/message-id/CA%2BrenyUFC13F0tYKxEENZtWA0YVuS5Tv%2BZQkEkAwuDO1-Xke-A%40mail.gmail.com 
I thought since we use SPI for foreign keys, it should be safe to use it here as well.

 > My suggestions is to close the commitfest entry as "committed" and start new threads and new entries
 > for the subsequent features.

Vignesh closed the current commitfest entry already, but I'll make a new one. The patches mostly 
depend on each other, so I'll make just one.

Rebased to 5941946d09.

Yours,

-- 
Paul              ~{:-)
[email-masked]
From 9d45f4113ccccb39797bca857e23470cdd401d2b Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v50 1/5] Add without_portion GiST support proc

This new support proc is used by UPDATE/DELETE FOR PORTION OF to
compute leftovers that weren't touched by the UPDATE/DELETE. This
commit defines implementations for ranges and multiranges.

Author: Paul Jungwirth
---

diff --git a/contrib/bloom/blvalidate.c b/contrib/bloom/blvalidate.c
index 001c188aeb7..6b14b2378ff 100644
--- a/contrib/bloom/blvalidate.c
+++ b/contrib/bloom/blvalidate.c
@@ -96,7 +96,7 @@ blvalidate(Oid opclassoid)
 		switch (procform->amprocnum)
 		{
 			case BLOOM_HASH_PROC:
-				ok = check_amproc_signature(procform->amproc, INT4OID, false,
+				ok = check_amproc_signature(procform->amproc, INT4OID, false, false,
 											1, 1, opckeytype);
 				break;
 			case BLOOM_OPTIONS_PROC:
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index a373a8aa4b2..c1015238024 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -266,7 +266,7 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
 
  <para>
    <filename>src/include/nodes/primnodes.h</filename>) into strategy numbers
 
  <variablelist>
@@ -1241,6 +1244,108 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 05361962495..42a92131ba0 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
index 915b8628b46..f5cae491f4a 100644
@@ -80,21 +80,21 @@ brinvalidate(Oid opclassoid)
index 5b0bfe8cc1d..3abd8a34b1a 100644
@@ -97,37 +97,37 @@ ginvalidate(Oid opclassoid)
index 2a49e6d20f0..b8a6796ea20 100644
@@ -98,36 +98,36 @@ gistvalidate(Oid opclassoid)
@@ -135,15 +135,19 @@ gistvalidate(Oid opclassoid)
 											1, 1, INT4OID) &&
 					procform->amproclefttype == ANYOID &&
 					procform->amprocrighttype == ANYOID;
@@ -265,7 +269,7 @@ gistvalidate(Oid opclassoid)
@@ -337,6 +341,7 @@ gistadjustmembers(Oid opfamilyoid,
index 06ac832ba10..902d7645ba8 100644
@@ -96,11 +96,11 @@ hashvalidate(Oid opclassoid)
index 4cf237019ad..fd7b653716a 100644

 	va_start(ap, maxargs);
 
 /*
index dd6f5a15c65..90fa0245a18 100644
@@ -83,16 +83,16 @@ btvalidate(Oid opclassoid)
@@ -100,7 +100,7 @@ btvalidate(Oid opclassoid)
index e9964fab4f4..b0cc6b50c08 100644
@@ -101,7 +101,7 @@ spgvalidate(Oid opclassoid)
@@ -156,11 +156,11 @@ spgvalidate(Oid opclassoid)
@@ -169,7 +169,7 @@ spgvalidate(Oid opclassoid)
index cd84ced5b48..dd7d05aa0f3 100644
 
+/*
index 5f9fb23871a..433543ada06 100644

+/*
 
 Datum
index 43b1692b079..cea95284e94 100644
 
index db78e60eeab..33c317b51bf 100644
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 19100482ba4..9feeb572bdf 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -609,6 +609,9 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
   amproc => 'gist_stratnum_common' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '13',
+  amproc => 'range_without_portion(anyrange,anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -649,6 +652,9 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
   amproc => 'gist_stratnum_common' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '13',
+  amproc => 'multirange_without_portion(anymultirange,anymultirange)' },
 
 # gin
index 890822eaf79..e5cef938cc6 100644
@@ -10826,6 +10826,10 @@
+{ oid => '8412', descr => 'remove portion from range',
@@ -11113,6 +11117,10 @@
+{ oid => '8411', descr => 'remove portion from multirange',
index 50adb3c8c13..34e7790fee3 100644

+-- without_portion

+-- without_portion
 
2.39.5

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

- 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 "leftover" part of
  a record touched by a FOR PORTION OF query.
- Added tg_temporal descriptor to the TriggerData struct that we pass to
  trigger functions. Our foreign key triggers use this to learn what
  bounds were given in the FOR PORTION OF clause.
- Documented FOR PORTION OF.
- Documented tg_temporal struct.
- Added tests.

Author: Paul Jungwirth
---
 src/backend/catalog/pg_constraint.c           |  47 +-
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/parser/analyze.c                  | 246 +++++-
 src/backend/utils/adt/ri_triggers.c           |  14 +-
 src/include/nodes/plannodes.h                 |   2 +
 .../regress/expected/without_overlaps.out     | 245 +++++-
 src/test/regress/sql/without_overlaps.sql     | 120 ++-
 48 files changed, 3298 insertions(+), 113 deletions(-)

index bb4ed3059c4..1e0aaf357c3 100644
 
 -- Test that alteration of server options causes reconnection
 -- Remote's errors might be non-English, so hide them to ensure stable results
@@ -6176,6 +6198,27 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass;
  ft2
 (1 row)
 
index d45e9f8ab52..b9c52740587 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";

@@ -1511,6 +1534,17 @@ EXPLAIN (verbose, costs off)
 
index 73f0c8d89fb..e71a73cfdc0 100644
@@ -367,6 +367,12 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
 
+  <para>
index 29649f6afd6..29633797c29 100644
     [ RETURNING [ WITH ( { OLD | NEW } AS <replaceable class="parameter">output_alias</replaceable> [, ...] ) ]
@@ -55,6 +57,39 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
@@ -117,6 +152,41 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
index 12ec5ba0709..2c6f88ecf81 100644
@@ -52,6 +54,41 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
@@ -115,6 +152,57 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>
index e9214dcf1b1..e6d21d2489d 100644
@@ -374,6 +374,15 @@
 
+   <para>
@@ -555,17 +564,18 @@ CALLED_AS_TRIGGER(fcinfo)
 
@@ -833,6 +843,38 @@ typedef struct Trigger
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry>
 
index ac80652baf2..2467461d716 100644
@@ -1620,12 +1620,19 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
@@ -1667,6 +1674,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:
@@ -1678,6 +1696,27 @@ FindFKPeriodOpers(Oid opclass,
 		default:
 			elog(ERROR, "unexpected opcintype: %u", opcintype);
 	}
+
+	/*
+	 * Look up the intersect proc. We use this for FOR PORTION OF
+	 * (both the operation itself and when checking foreign keys).
+	 * If this is missing we don't need to complain here,
+	 * because FOR PORTION OF will not be allowed.
+	 */
+	*intersectprocoid = get_opcode(*intersectoperoid);
+
+	/*
+	 * Look up the without_portion func. We need this for RESTRICT
+	 * foreign keys and also FOR PORTION OF.
+	 */
+	*withoutportionoid = InvalidOid;
+	*withoutportionoid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_WITHOUT_PORTION_PROC);
+	if (!OidIsValid(*withoutportionoid))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("could not identify a without_overlaps support function for type %s", format_type_be(opcintype)),
+				errhint("Define a without_overlaps support function for operator class \"%d\" for access method \"%s\".",
+						opclass, "gist"));
 }
 
 /*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..01cd29cfedb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10359,9 +10359,12 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
 		Oid			intersectoperoid;
+		Oid			intersectprocoid;
+		Oid			withoutoverlapsoid;

@@ -13061,6 +13064,7 @@ validateForeignKeyConstraint(char *conname,

index c9f61130c69..c1cee1e34eb 100644
@@ -48,12 +48,14 @@
@@ -2638,6 +2640,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
@@ -2737,6 +2740,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
@@ -2828,6 +2832,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo,

@@ -2891,6 +2896,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];
@@ -3026,6 +3032,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++)
@@ -3177,6 +3184,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 		TRIGGER_EVENT_ROW |
 		TRIGGER_EVENT_INSTEAD;
 	LocTriggerData.tg_relation = relinfo->ri_RelationDesc;
+	LocTriggerData.tg_temporal = relinfo->ri_forPortionOf;

@@ -3646,6 +3654,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;
@@ -3919,6 +3928,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);

@@ -4126,6 +4136,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,
@@ -4502,6 +4513,7 @@ AfterTriggerExecute(EState *estate,
 	LocTriggerData.tg_relation = rel;
 	if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype))
 		LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols;
+	LocTriggerData.tg_temporal = evtshared->for_portion_of;

@@ -6066,6 +6078,40 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6482,6 +6528,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 		new_shared.ats_modifiedcols = modifiedCols;
 
index e9bd98c7738..0571d5d9934 100644
@@ -1377,6 +1377,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index 87c820276a8..e964b463dfa 100644
 
 #include "postgres.h"

@@ -130,6 +136,19 @@ typedef struct UpdateContext
 	LockTupleMode lockmode;
 } UpdateContext;
 
+/*
+ * FPO_QueryHashEntry
+ */
 
@@ -151,6 +170,13 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
@@ -173,6 +199,7 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,

 /*
@@ -1334,6 +1361,367 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+				if (tupdesc->compact_attrs[i].attisdropped || tupdesc->compact_attrs[i].attgenerated)
+				types[i] = TupleDescAttr(tupdesc, i)->atttypid;
+			if (tupdesc->compact_attrs[i].attisdropped || tupdesc->compact_attrs[i].attgenerated)
@@ -1486,7 +1874,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1519,6 +1908,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1944,7 +2337,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2310,6 +2707,10 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4916,6 +5317,99 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index 3288396def3..72b98cf2957 100644
@@ -765,7 +765,7 @@ SPI_execute_plan_with_paramlist(SPIPlanPtr plan, ParamListInfo params,
index 7bc823507f1..89be5ec0db8 100644
@@ -2571,6 +2571,14 @@ expression_tree_walker_impl(Node *node,
@@ -2719,6 +2727,8 @@ query_tree_walker_impl(Query *query,
@@ -3613,6 +3623,19 @@ expression_tree_mutator_impl(Node *node,
@@ -3794,6 +3817,7 @@ query_tree_mutator_impl(Query *query,
index 75e2b0b9036..788d330e123 100644
 
@@ -2832,6 +2832,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);
@@ -7106,7 +7107,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;
@@ -7174,6 +7175,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 141177e7413..4b4619b1b60 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2065,6 +2065,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 										parse->onConflict,
 										mergeActionLists,
 										mergeJoinConditions,
+										parse->forPortionOf,
 										assign_special_exec_param(root));
 		}
 
index 93e73cb44db..b8322dbc84a 100644
@@ -3884,7 +3884,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3951,6 +3951,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
index 76f58b3aca3..71d54d8dc1c 100644
@@ -24,7 +24,11 @@
 
 #include "postgres.h"
 
+#include "catalog/pg_operator.h"
@@ -47,10 +51,12 @@
 
@@ -59,10 +65,16 @@
 
@@ -567,6 +579,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -600,6 +626,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -638,7 +665,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

 	transformReturningClause(pstate, qry, stmt->returningClause,
@@ -1273,7 +1304,7 @@ transformOnConflictClause(ParseState *pstate,
 		 * Now transform the UPDATE subexpressions.
 		 */
 		onConflictSet =
-			transformUpdateTargetList(pstate, onConflictClause->targetList);
+			transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1303,6 +1334,190 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+	GetOperatorFromCompareType(opclass, InvalidOid, COMPARE_OVERLAP, &opid, &strat);
+		 * Whatever operator is used for intersect by temporal foreign keys,
+		 * we can use its backing procedure for intersects in FOR PORTION OF.
+		 * For now foreign keys hardcode operators for range and multirange,
+		 * so this we just duplicate the logic from FindFKPeriodOpersAndProcs.
+				intersectoperoid = OID_RANGE_INTERSECT_RANGE_OP;
+				intersectoperoid = OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP;
+		funcid = get_opcode(intersectoperoid);
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2511,6 +2726,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2528,6 +2744,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2544,7 +2764,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

 	transformReturningClause(pstate, qry, stmt->returningClause,
@@ -2554,7 +2775,7 @@ 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);
 
@@ -2573,7 +2794,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2626,6 +2847,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 271ae26cbaf..218674f7952 100644
@@ -246,6 +246,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -548,6 +549,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -757,7 +760,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER

@@ -876,12 +879,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -12439,6 +12445,20 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+					n->returningClause = $9;
+					n->withClause = $1;
+					$$ = (Node *) n;
+				}
 		;
 
 using_clause:
@@ -12514,6 +12534,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
 					n->stmt_location = @$;
 					$$ = (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 = $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:
@@ -14017,6 +14056,44 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+opt_alias:
+			AS ColId
+				{
@@ -14850,16 +14927,25 @@ opt_timezone:
 
+/*
@@ -17913,6 +17999,7 @@ unreserved_keyword:
@@ -18540,6 +18627,7 @@ bare_label_keyword:
index 0ac8966e30f..8d1105dfddb 100644
 
 			/*
 
 			/*
index d2e218353f3..522345b1668 100644
index 9caf1e481a2..7f5158c2b7c 100644
 
 			/*
 
 			/*
@@ -3169,6 +3175,8 @@ ParseExprKindName(ParseExprKind exprKind)
index 583bbbf232f..9d4e73fe192 100644
 
 			/*
index 51d7703eff7..ed276c41460 100644
index f0bce5f9ed9..a9b52069922 100644
@@ -3728,6 +3728,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
@@ -4067,6 +4091,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
index c4ff18ce65e..02d26237b76 100644
@@ -130,7 +130,9 @@ typedef struct RI_ConstraintInfo
 
@@ -2339,10 +2341,12 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	{
 		Oid			opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);

 	ReleaseSysCache(tup);
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 82031c1e8e5..0acc5a7d629 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2271,6 +2271,33 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
index 6da164e7e4d..3a1f1ddbe5c 100644
@@ -288,10 +288,12 @@ extern void DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 
index 4180601dcd4..559d59a2e24 100644
 
 /*
index d4d4e655180..6525bb873d2 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -447,6 +448,26 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -577,6 +598,9 @@ typedef struct ResultRelInfo

index 23c9e3c5abf..39de24fd2c2 100644
 
@@ -1591,6 +1594,21 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -2108,6 +2126,7 @@ typedef struct DeleteStmt
 
@@ -2125,6 +2144,7 @@ typedef struct UpdateStmt
 
 /* ----------------------
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index c24a1fc8514..2d3a874f5f0 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -2423,6 +2423,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 f78bffd90cf..f3a55daea7a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -325,6 +325,8 @@ typedef struct ModifyTable
 	List	   *onConflictCols;
 	/* WHERE for ON CONFLICT 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 7d3b4198f26..c186a3babd6 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2365,4 +2365,30 @@ typedef struct OnConflictExpr
 	List	   *exclRelTlist;	/* tlist of the EXCLUDED pseudo relation */
 } OnConflictExpr;
 
+/*----------
index 719be3897f6..05c0f9b5cdb 100644
@@ -288,7 +288,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
index f1bd18c49f2..5008f6dce5b 100644
 extern void transformReturningClause(ParseState *pstate, Query *qry,
 									 ReturningClause *returningClause,
index 40cf090ce61..d39e79fa983 100644
index 994284019fb..f30a5033933 100644
index d42380a0d46..0c655228aef 100644
@@ -150,6 +150,7 @@ extern Oid	get_rel_relam(Oid relid);
index 954f549555e..a56df974bfc 100644
@@ -1142,6 +1142,24 @@ ERROR:  null value in column "b" of relation "errtst_part_2" violates not-null c
index 095df0a670c..dc098f5b443 100644
@@ -3700,6 +3700,38 @@ select * from uv_iocu_tab;
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index e38472079cc..ffaa18e9e0b 100644

 
 
+  FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+  SET id = '[7,8)'
+  FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+  SET id = '[7,8)'
@@ -1802,6 +1893,42 @@ BEGIN;
@@ -1818,11 +1945,12 @@ ALTER TABLE temporal_fk_rng2rng
   ON DELETE RESTRICT;
 ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
@@ -1830,8 +1958,9 @@ ALTER TABLE temporal_fk_rng2rng
 ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
@@ -1839,9 +1968,10 @@ ALTER TABLE temporal_fk_rng2rng
 ERROR:  unsupported ON UPDATE action for foreign key constraint using PERIOD
@@ -2211,6 +2341,22 @@ UPDATE temporal_mltrng SET id = '[7,8)'
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+  SET id = '[7,8)'
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+  SET id = '[7,8)'
@@ -2253,6 +2399,19 @@ BEGIN;
index 0a35f2f8f6a..180a8533aab 100644
 
 # ----------
index b81694c24f2..4658fb8081f 100644
@@ -774,6 +774,24 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa';
 
index c071fffc116..e8c04e3ad91 100644
@@ -1881,6 +1881,20 @@ select * from uv_iocu_tab;
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
 
index 4aaca242bbe..c5c89fe40ab 100644

 
 
 -- temporal UNIQUE:
 
+  FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+  SET id = '[7,8)'
+  FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+  SET id = '[7,8)'
@@ -1338,6 +1392,18 @@ BEGIN;
 
@@ -1356,12 +1422,13 @@ ALTER TABLE temporal_fk_rng2rng
   ON DELETE RESTRICT;

@@ -1369,8 +1436,9 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -1378,9 +1446,10 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -1716,6 +1785,20 @@ BEGIN;
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+  SET id = '[7,8)'
+  FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+  SET id = '[7,8)'
 
@@ -1760,6 +1843,17 @@ BEGIN;

index 6bbf6567279..b74693cb89c 100644
@@ -137,6 +137,12 @@ is( $stderr,
 
@@ -144,6 +150,12 @@ is( $stderr,

@@ -165,16 +177,22 @@ $node_publisher->safe_psql(

 

@@ -192,6 +210,12 @@ is( $stderr,
 
@@ -199,6 +223,12 @@ is( $stderr,

@@ -287,16 +317,22 @@ $node_publisher->safe_psql(

 

 # replicate with a primary key:
@@ -310,16 +346,22 @@ $node_publisher->safe_psql(

 

 # replicate with a unique key:
@@ -333,17 +375,23 @@ $node_publisher->safe_psql(

 

 # cleanup
 
@@ -425,16 +473,22 @@ $node_publisher->safe_psql(

 

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

 

 # cleanup
@@ -543,6 +603,12 @@ is( $stderr,
 
@@ -550,6 +616,12 @@ is( $stderr,

@@ -575,6 +647,12 @@ is( $stderr,
 
@@ -582,6 +660,12 @@ is( $stderr,

@@ -607,6 +691,12 @@ is( $stderr,
 
@@ -614,6 +704,12 @@ is( $stderr,

-- 
2.39.5

From b8b5e021dbba85a1e838c79db4355e424211dd9a Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v50 3/5] 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 Jungwirth
---
 doc/src/sgml/ref/create_table.sgml            |   14 +-
 src/backend/commands/tablecmds.c              |   65 +-
 src/backend/utils/adt/ri_triggers.c           |  615 ++++++-
 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 +++++++++-
 7 files changed, 3178 insertions(+), 50 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 5304b738322..870c2ddbd21 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1299,7 +1299,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1314,7 +1316,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
@@ -1331,7 +1336,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          </para>
 
          <para>
index 01cd29cfedb..6d340ce8111 100644
@@ -532,7 +532,7 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *
@@ -9870,6 +9870,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9955,15 +9956,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

@@ -10064,19 +10069,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",
@@ -10431,6 +10430,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 fkperiodattnum,
 							 int numfksetcols, const int16 *fksetcolsattnums,
 							 List *fksetcols)
 {
@@ -10441,6 +10441,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *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;
@@ -13192,17 +13199,26 @@ createForeignKeyActionTriggers(Relation rel, 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",
@@ -13253,17 +13269,26 @@ createForeignKeyActionTriggers(Relation rel, 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 02d26237b76..279fd2607c4 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -81,6 +81,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
 
@@ -196,6 +202,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
@@ -232,6 +239,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 							bool is_restrict,
@@ -241,6 +249,11 @@ pg_noreturn static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 										   int queryno, bool is_restrict, bool partgone);

 /*
@@ -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,538 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 }

+/*
+					false,
+					false,
+					false,
@@ -2489,6 +3040,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 				bool is_restrict,
@@ -2501,8 +3053,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -2545,6 +3097,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -3225,6 +3783,12 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 		case F_RI_FKEY_CHECK_INS:
@@ -3234,3 +3798,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index e5cef938cc6..948605adfd7 100644
@@ -4070,6 +4070,28 @@
 
index 8879554c3f7..4bdf8ed6bab 100644
@@ -385,14 +385,17 @@ select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
 (3 rows)

 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
@@ -431,14 +434,17 @@ select proname from pg_proc where proname like E'RI\\_FKey%del' order by 1;
 (6 rows)

 explain (costs off)
index ffaa18e9e0b..11cc8a01441 100644
@@ -1947,7 +1947,24 @@ ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
 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
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', 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)');
@@ -2413,6 +2994,626 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02
@@ -2421,8 +3622,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -2435,8 +3636,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -2478,7 +3679,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -2490,37 +3691,162 @@ INSERT INTO temporal_partitioned_fk_rng2rng (id, valid_at, parent_id) VALUES ('[
@@ -2528,10 +3854,73 @@ ALTER TABLE temporal_partitioned_fk_rng2rng
@@ -2617,32 +4006,150 @@ DETAIL:  Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenc
@@ -2650,10 +4157,67 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng
index c5c89fe40ab..224ddef8430 100644
@@ -1424,8 +1424,26 @@ ALTER TABLE temporal_fk_rng2rng
 
+--
 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
 
+--
 INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', 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)');
 
 --
 -- 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'));
 
+--
+
+--
@@ -1865,8 +2603,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -1880,8 +2618,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
 
 --
 -- 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
 --

 

 
 
@@ -1977,11 +2769,34 @@ ALTER TABLE temporal_partitioned_fk_rng2rng

 
@@ -2070,36 +2885,90 @@ DELETE FROM temporal_partitioned_mltrng WHERE id = '[5,6)' AND valid_at = datemu
 -- partitioned FK referenced updates CASCADE
 --

 

 
 
@@ -2107,11 +2976,34 @@ ALTER TABLE temporal_partitioned_fk_mltrng2mltrng

 
-- 
2.39.5

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

Author: Paul Jungwirth
---

index e937491e6b8..f5199872e2e 100644
@@ -4247,6 +4247,30 @@ ASSERT <replaceable class="parameter">condition</replaceable> <optional> , <repl
 
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index f36a244140e..9a1a4085f44 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -724,6 +724,33 @@ do_compile(FunctionCallInfo fcinfo,
 			var->dtype = PLPGSQL_DTYPE_PROMISE;
 			((PLpgSQL_var *) var)->promise = PLPGSQL_PROMISE_TG_ARGV;

 		case PLPGSQL_EVENT_TRIGGER:
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index d4377ceecbf..7925580998a 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -1385,6 +1385,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 						PLpgSQL_var *var)
 {
 	MemoryContext oldcontext;
+	ForPortionOfState *fpo;
 
 	if (var->promise == PLPGSQL_PROMISE_NONE)
 		return;					/* nothing to do */
@@ -1516,6 +1517,37 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate,
 			}
 			break;
 
index aea0d0f98b2..63a34027561 100644
 
 /*
2.39.5

From 36f4aa1a720b8d6d607bd88d75f1ece09f3434be Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v50 5/5] Add PERIODs

Author: Paul Jungwirth
---
 src/backend/commands/tablecmds.c              |  879 +++-
 src/backend/parser/analyze.c                  |  128 +-
 src/backend/parser/parse_utilcmd.c            |  172 +-
 src/test/regress/expected/periods.out         |  275 +
 .../regress/expected/without_overlaps.out     | 4500 ++++++++++++++++-
 src/test/regress/sql/periods.sql              |  186 +
 src/test/regress/sql/without_overlaps.sql     | 3063 ++++++++++-
 64 files changed, 10487 insertions(+), 202 deletions(-)

index fb050635551..d48d32534ea 100644
 
+     <row>
@@ -5776,6 +5781,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
index cdb1a07e9d3..722637d6f5d 100644
@@ -1436,6 +1436,64 @@ CREATE TABLE circles (
   </sect2>
  </sect1>

index 19dffe7be6a..ce6b75494ff 100644

diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index f5199872e2e..dab9864c94b 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -4252,7 +4252,7 @@ ASSERT <replaceable class="parameter">condition</replaceable> <optional> , <repl
      <term><varname>TG_PERIOD_NAME</varname> <type>text</type></term>
      <listitem>
       <para>
-       the column name used in a <literal>FOR PORTION OF</literal> clause,
+       the column/period name used in a <literal>FOR PORTION OF</literal> clause,
        or else <symbol>NULL</symbol>.
       </para>
      </listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..f70b7282f77 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -62,6 +62,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     ALTER CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> SET [ INHERIT | NO INHERIT ]
     VALIDATE CONSTRAINT <replaceable class="parameter">constraint_name</replaceable>
     DROP CONSTRAINT [ IF EXISTS ]  <replaceable class="parameter">constraint_name</replaceable> [ RESTRICT | CASCADE ]
+    ADD PERIOD FOR <replaceable class="parameter">period_name</replaceable> ( <replaceable class="parameter">start_column</replaceable>, <replaceable class="parameter">end_column</replaceable> ) [ WITH ( <replaceable class="parameter">period_option</replaceable> = <replaceable class="parameter">value</replaceable> [, ... ] ) ]
+    DROP PERIOD FOR <replaceable class="parameter">period_name</replaceable> [ RESTRICT | CASCADE ]
     DISABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE TRIGGER [ <replaceable class="parameter">trigger_name</replaceable> | ALL | USER ]
     ENABLE REPLICA TRIGGER <replaceable class="parameter">trigger_name</replaceable>
@@ -116,10 +118,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
-  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
-  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ]  [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ]  [, PERIOD { <replaceable class="parameter">refcolumn</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
 
@@ -611,6 +613,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index 870c2ddbd21..fd0f8790134 100644
@@ -73,15 +76,20 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]

-  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
+  FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">refcolumn</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
 [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] [ ENFORCED | NOT ENFORCED ]
 
+  <para>
@@ -805,6 +821,37 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
@@ -1168,8 +1215,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
-   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] )
-    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">refcolumn</replaceable> ] ) ]
+   <term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">column_name</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] )
+    REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD { <replaceable class="parameter">refcolumn</replaceable> | <replaceable class="parameter">period_name</replaceable> } ] ) ]
     [ MATCH <replaceable class="parameter">matchtype</replaceable> ]
     [ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
     [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1189,7 +1236,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
@@ -1197,8 +1244,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
index 29633797c29..dc728cce187 100644
@@ -58,7 +58,8 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
   </para>
 
   <para>
index 2c6f88ecf81..05e2dcb472b 100644
@@ -55,7 +55,8 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ]
   </para>
 
   <para>
index e6d21d2489d..7b4975fea47 100644
@@ -859,17 +859,22 @@ typedef struct ForPortionOfState
 {
     NodeTag type;

index c090094ed08..53eb67034f3 100644
index 02a754cc30a..9cf91495963 100644
@@ -2744,6 +2744,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
@@ -2885,6 +2886,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
index 18316a3968b..8559d421318 100644
 
+					/*
 
+		case PeriodRelationId:
index bd3554c0bfd..f0fe0b94fa7 100644
@@ -2125,6 +2126,80 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 
+/*
index a7bffca93d1..282ead6ed4c 100644

 /*
index 1958ea9238a..6cdf89d0e93 100644
index d8eb8d3deaa..4ad6902c7ef 100644
@@ -971,6 +975,7 @@ get_object_address(ObjectType objtype, Node *object,
@@ -1473,6 +1478,13 @@ get_object_address_relobject(ObjectType objtype, List *object,
@@ -2290,6 +2302,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
@@ -2400,6 +2413,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
@@ -3073,6 +3087,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
@@ -4520,6 +4566,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 										 missing_ok);
 			break;
 
+		case PeriodRelationId:
+			appendStringInfoString(&buffer, "period");
+			break;
+
 		case ConversionRelationId:
 			appendStringInfoString(&buffer, "conversion");
 			break;
@@ -5025,6 +5075,28 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case PeriodRelationId:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 2f250d2c57b..6e3af2d8dcc 100644
index f67a8b95d29..7110afb352f 100644
index ceb9a229b63..526c3ff5142 100644
index edc2c988e29..36706e9a100 100644
index cee5d7bbb9c..f8650eb4fe4 100644
index 6d340ce8111..213bd9527ed 100644
@@ -374,6 +380,7 @@ static List *MergeAttributes(List *columns, const List *supers, char relpersiste
 static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr, bool is_enforced);
@@ -468,6 +475,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
@@ -485,6 +494,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -710,6 +725,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -939,6 +958,82 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
+					 * XXX: We should check the GENERATED expression also,
+					 * but that is hard to do for non-range/multirange PERIODs.
@@ -1307,7 +1402,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
 	/*
-	 * Finally, merge the not-null constraints that are declared directly with
+	 * Now merge the not-null constraints that are declared directly with
 	 * those that come from parent relations (making sure to count inheritance
 	 * appropriately for each), create them, and set the attnotnull flag on
 	 * columns that don't yet have it.
@@ -1317,6 +1412,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	foreach_int(attrnum, nncols)
 		set_attnotnull(NULL, rel, attrnum, NoLock);
 
+	/*
+	 * Finally, create periods for the table. This must come after we create
+	 * columns and before we create index constraints. It will automatically
+	 * create a CHECK constraint for the period.
+	 */
+	foreach(listptr, stmt->periods)
+	{
+		PeriodDef *period = (PeriodDef *) lfirst(listptr);
+
+		/* Don't update the count of check constraints twice */
+		CommandCounterIncrement();
+
+		AddRelationNewPeriod(rel, period);
+	}
+
 	ObjectAddressSet(address, RelationRelationId, relationId);
 
 	/*
@@ -1416,6 +1526,316 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
+	constr->is_enforced = true;
+	col->is_not_null = false;
+		 * XXX: We should check the GENERATED expression also,
+		 * but that is hard to do for non-range/multirange PERIODs.
@@ -3109,6 +3529,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4516,12 +5098,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4530,7 +5112,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4623,6 +5205,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4942,6 +5526,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5355,6 +5947,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6505,6 +7105,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6528,6 +7130,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7544,14 +8148,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7595,6 +8214,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
+	/* XXX: implement IF [NOT] EXISTS for periods */
@@ -8120,6 +8810,161 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -10029,8 +10874,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
-		if (attgenerated)
+		if (attgenerated && !bms_is_member(fkattnum[i], periods))
 		{
 			/*
 			 * Check restrictions on UPDATE/DELETE actions, per SQL standard
@@ -14432,6 +15278,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 				RememberConstraintForRebuilding(foundObject.objectId, tab);
 				break;
 
+			case PeriodRelationId:
 
 				/*
@@ -14521,6 +15377,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 					}
 					else
 					{
+						/*
+						 * If this GENERATED column is implementing a PERIOD,
+						 * keep going and we'll fail from the PERIOD instead.
+						 * This gives a more clear error message.
+						 */
+						Bitmapset *periodatts = get_period_attnos(RelationGetRelid(rel));
+						if (bms_is_member(col.objectSubId, periodatts))
+							break;
+
 						/*
 						 * This must be a reference from the expression of a
 						 * generated column elsewhere in the same table.
@@ -16434,7 +17299,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

index c1cee1e34eb..cc93648badc 100644
@@ -6103,6 +6103,8 @@ CopyForPortionOfState(ForPortionOfState *src)
 
index 6f0301555e0..c7276836220 100644

 
 
 		/*
 		 * There is very little to do here to update the view's dependencies.
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index e964b463dfa..fae2e705a60 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1491,6 +1491,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	FmgrInfo flinfo;
 	ReturnSetInfo rsi;
 	Relation rel;
+	bool hasPeriod = false;
 	bool didInit = false;
 	bool shouldFree = false;
 	LOCAL_FCINFO(fcinfo, 2);
@@ -1600,6 +1601,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 			else
 				leftoverSlot = oldtupleSlot;

@@ -1700,8 +1702,42 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 				fpoState->fp_nulls[i] = leftoverSlot->tts_isnull[i] ? 'n' : ' ';
 		}

index 89be5ec0db8..4763f21e33b 100644
@@ -1733,6 +1733,9 @@ exprLocation(const Node *expr)
 			loc = ((const FunctionParameter *) expr)->location;
index 71d54d8dc1c..2898d0de59e 100644
 
+	/*
 
 	if (forPortionOf->target)
 		/*
@@ -1465,7 +1517,10 @@ transformForPortionOfClause(ParseState *pstate,
 	{
 		/*
 		 * Now make sure we update the start/end time of the record.
-		 * For a range col (r) this is `r = r * targetRange`.
+		 * For a range/etc col (r) this is `r = r * targetRange`.
+		 * For a PERIOD with cols (s, e) this is `s = lower(tsrange(s, e) * targetRange)`
+		 * and `e = upper(tsrange(s, e) * targetRange` (of course not necessarily with
+		 * tsrange, but with whatever range type is used there).
 		 */
 		Oid				intersectoperoid;
 		List		   *funcArgs = NIL;
@@ -1502,14 +1557,71 @@ transformForPortionOfClause(ParseState *pstate,
 		rangeTLEExpr = makeFuncExpr(funcid, attr->atttypid, funcArgs,
 									InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
 
-		tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno, range_name, false);
 
-		/* Mark the range column as requiring update permissions */
-		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
-													  range_attno - FirstLowInvalidHeapAttributeNumber);
+		if (result->startVar)
+		{
+			FuncExpr *boundTLEExpr;
+			Oid arg_types[1] = {ANYRANGEOID};
+			FuncDetailCode fdresult;
+			Oid rettype;
+			bool retset;
+			int nvargs;
+			Oid vatype;
+			Oid *declared_arg_types;
+			Oid elemtypid = get_range_subtype(attr->atttypid);
+
+			/* set the start column */
+			fdresult = func_get_detail(SystemFuncName("lower"), NIL, NIL, 1,
+									   arg_types,
+									   false, false, false,
+									   &funcid, &rettype, &retset,
+									   &nvargs, &vatype,
+									   &declared_arg_types, NULL);
+			if (fdresult != FUNCDETAIL_NORMAL)
+				elog(ERROR, "failed to find lower(anyrange) function");
+			boundTLEExpr = makeFuncExpr(funcid,
+										elemtypid,
+										list_make1(rangeTLEExpr),
+										InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
+			tle = makeTargetEntry((Expr *) boundTLEExpr, start_attno, startcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* set the end column */
+			fdresult = func_get_detail(SystemFuncName("upper"), NIL, NIL, 1,
+									   arg_types,
+									   false, false, false,
+									   &funcid, &rettype, &retset,
+									   &nvargs, &vatype,
+									   &declared_arg_types, NULL);
+			if (fdresult != FUNCDETAIL_NORMAL)
+				elog(ERROR, "failed to find upper(anyrange) function");
+			boundTLEExpr = makeFuncExpr(funcid,
+										elemtypid,
+										list_make1(rangeTLEExpr),
+										InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
+			tle = makeTargetEntry((Expr *) boundTLEExpr, end_attno, endcolname, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/*
+			 * Mark the start/end columns as requiring update permissions.
+			 * As usual, we don't check permissions for the GENERATED column.
+			 */
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  start_attno - FirstLowInvalidHeapAttributeNumber);
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  end_attno - FirstLowInvalidHeapAttributeNumber);
+		}
+		else
+		{
+			tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno, range_name, false);
+			result->rangeTargetList = lappend(result->rangeTargetList, tle);
+
+			/* Mark the range column as requiring update permissions */
+			target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+														  range_attno - FirstLowInvalidHeapAttributeNumber);
+		}
 	}
 	else
 		result->rangeTargetList = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 218674f7952..068ae7a4bbc 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -593,7 +593,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <keyword> col_name_keyword reserved_keyword
 %type <keyword> bare_label_keyword
 
@@ -2650,6 +2650,24 @@ alter_table_cmd:
@@ -3811,8 +3829,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4190,6 +4210,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7305,6 +7338,14 @@ CommentStmt:
@@ -17995,7 +18036,6 @@ unreserved_keyword:
@@ -18300,6 +18340,7 @@ reserved_keyword:
index 04ecf64b1fc..7abd7b4ee14 100644
@@ -3293,6 +3294,7 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
@@ -3316,12 +3318,20 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,

index 896a7f2c59b..7fc821a55ad 100644
@@ -110,6 +112,8 @@ typedef struct
 
@@ -279,6 +284,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
+			case T_PeriodDef:
@@ -367,6 +376,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
@@ -1041,6 +1051,91 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
@@ -1142,6 +1237,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1187,6 +1283,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1196,10 +1293,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
@@ -2602,6 +2707,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
@@ -2620,6 +2726,24 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			if (!found)
 				column = NULL;
 
+			/* If the key is WITHOUT OVERLAPS, a PERIOD will work too. */
+			if (!found && constraint->without_overlaps &&
+				lc == list_last_cell(constraint->keys))
+			{
+				PeriodDef  *period = NULL;
+
+				foreach(columns, cxt->periods)
+				{
+					period = lfirst_node(PeriodDef, columns);
+					if (strcmp(period->periodname, key) == 0)
+					{
+						found = true;
+						hasperiod = true;
+						break;
+					}
+				}
+			}
+
 			if (found)
 			{
 				/*
@@ -2636,24 +2760,34 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				if (constraint->contype == CONSTR_PRIMARY &&
 					!cxt->isalter)
 				{
-					if (column->is_not_null)
+					if (column)
 					{
-						foreach_node(Constraint, nn, cxt->nnconstraints)
+						if (column->is_not_null)
 						{
-							if (strcmp(strVal(linitial(nn->keys)), key) == 0)
+							foreach_node(Constraint, nn, cxt->nnconstraints)
 							{
-								if (nn->is_no_inherit)
-									ereport(ERROR,
-											errcode(ERRCODE_SYNTAX_ERROR),
-											errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"",
-												   key));
-								break;
+								if (strcmp(strVal(linitial(nn->keys)), key) == 0)
+								{
+									if (nn->is_no_inherit)
+										ereport(ERROR,
+												errcode(ERRCODE_SYNTAX_ERROR),
+												errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"",
+													   key));
+									break;
+								}
 							}
 						}
+						else
+						{
+							column->is_not_null = true;
+							cxt->nnconstraints =
+								lappend(cxt->nnconstraints,
+										makeNotNullConstraint(makeString(key)));
+						}
 					}
-					else
+					else if (hasperiod)
 					{
-						column->is_not_null = true;
+						/* If we're using a PERIOD, we better make sure it is NOT NULL */
 						cxt->nnconstraints =
 							lappend(cxt->nnconstraints,
 									makeNotNullConstraint(makeString(key)));
@@ -2780,7 +2914,12 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 						}
 					}
 				}
-				if (found)
+				/*
+				 * In CREATE TABLE we don't know PERIODs' rangetype yet,
+				 * but we know it will be a range/multirange. So if we
+				 * have a PERIOD then we're safe.
+				 */
+				if (found && !hasperiod)
 				{
 					if (!OidIsValid(typid) && column)
 						typid = typenameTypeId(NULL, column->typeName);
@@ -3104,6 +3243,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 		}
 	}
 
@@ -3561,6 +3704,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
index 35e8c01aab9..89fa17b1e94 100644
 	pg_locale_builtin.o \
index 0acc5a7d629..89539849a02 100644
@@ -32,6 +32,7 @@
@@ -1030,6 +1031,68 @@ get_attoptions(Oid relid, int16 attnum)
 	return result;
 }

 /*
@@ -3608,6 +3671,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 82d51c89ac6..8618241beba 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3739,6 +3739,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 			 strcmp(type, "DATABASE PROPERTIES") == 0 ||
 			 strcmp(type, "DEFAULT") == 0 ||
 			 strcmp(type, "FK CONSTRAINT") == 0 ||
+			 strcmp(type, "PERIOD") == 0 ||
 			 strcmp(type, "INDEX") == 0 ||
 			 strcmp(type, "RULE") == 0 ||
 			 strcmp(type, "TRIGGER") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..6ed9af97388 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6889,6 +6889,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reltype;
 	int			i_relowner;
 	int			i_relchecks;
+	int			i_nperiod;
 	int			i_relhasindex;
 	int			i_relhasrules;
 	int			i_relpages;
@@ -6968,6 +6969,14 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
@@ -7105,6 +7114,7 @@ getTables(Archive *fout, int *numTables)
@@ -7192,6 +7202,7 @@ getTables(Archive *fout, int *numTables)
@@ -8873,7 +8884,7 @@ getTransforms(Archive *fout)
@@ -8925,6 +8936,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8939,7 +8952,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9345,15 +9359,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9375,6 +9410,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9394,12 +9430,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9458,6 +9495,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -11063,6 +11174,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16675,6 +16788,33 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16683,7 +16823,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16995,7 +17135,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			 */
 			resetPQExpBuffer(extra);
 			firstitem = true;
-			for (k = 0; k < tbinfo->ncheck; k++)
+			for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++)
 			{
 				ConstraintInfo *constr = &(tbinfo->checkexprs[k]);
 
@@ -17289,7 +17429,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -19362,6 +19502,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_TRIGGER:
 			case DO_EVENT_TRIGGER:
 			case DO_DEFAULT_ACL:
+			case DO_PERIOD:
 			case DO_POLICY:
 			case DO_PUBLICATION:
 			case DO_PUBLICATION_REL:
index bbdb30b5f54..579cd161d4d 100644
@@ -60,6 +60,7 @@ typedef enum
@@ -315,12 +316,14 @@ typedef struct _tableInfo
@@ -351,6 +354,7 @@ typedef struct _tableInfo
@@ -369,6 +373,7 @@ typedef struct _tableInfo
 	bool	   *notnull_islocal;	/* true if NOT NULL has local definition */
 
@@ -515,6 +520,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
index f75e9928bad..26186f8394d 100644
@@ -1409,6 +1411,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
index e6cf468ac9e..cfec099e037 100644
@@ -1985,6 +1985,8 @@ describeOneTableDetails(const char *schemaname,

 	res = PSQLexec(buf.data);
@@ -2413,6 +2415,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */
index 2bbc7805fe3..b9aee2ca49e 100644
index dbd339e9df4..d8f07289ef5 100644
@@ -125,6 +125,10 @@ extern void StoreAttrMissingVal(Relation rel, AttrNumber attnum,
 								Datum missingval);
 
index ec1cf467f6f..87cb4ce3300 100644
index 4392b9d221d..a64b2c2549f 100644
index 3bba6162782..89e60d6cbf1 100644

 
 /*
index 6832470d387..fae541e66ed 100644

 

 #endif							/* TABLECMDS_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6525bb873d2..5f724205348 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -457,9 +457,11 @@ typedef struct ForPortionOfState
 {
 	NodeTag		type;
 
-	char   *fp_rangeName;		/* the column named in FOR PORTION OF */
+	char   *fp_rangeName;		/* the column/PERIOD 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 */
+	int		fp_rangeAttno;		/* the attno of the range column (or 0 for a PERIOD) */
+	int		fp_periodStartAttno;	/* the attno of the PERIOD start column (or 0 for a range) */
+	int		fp_periodEndAttno;		/* the attno of the PERIOD end column (or 0 for a range) */
 	Datum	fp_targetRange;		/* the range/multirange/etc from FOR PORTION OF */
 	TypeCacheEntry *fp_leftoverstypcache;	/* type cache entry of the range */
 	TupleTableSlot *fp_Existing;		/* slot to store old tuple */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 39de24fd2c2..8d86d7d0f88 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2357,6 +2357,7 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_PARAMETER_ACL,
+	OBJECT_PERIOD,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
@@ -2444,6 +2445,8 @@ typedef enum AlterTableType
 	AT_ValidateConstraint,		/* validate constraint */
 	AT_AddIndexConstraint,		/* add constraint using existing index */
 	AT_DropConstraint,			/* drop constraint */
+	AT_AddPeriod,				/* ADD PERIOD */
+	AT_DropPeriod,				/* DROP PERIOD */
 	AT_ReAddComment,			/* internal to commands/tablecmds.c */
 	AT_AlterColumnType,			/* alter column type */
 	AT_AlterColumnGenericOptions,	/* alter column OPTIONS (...) */
@@ -2738,11 +2741,12 @@ typedef struct VariableShowStmt
 /* ----------------------
  *		Create Table Statement
  *
- * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints and nnconstraints are NIL.  After
+ * NOTE: in the raw gram.y output, ColumnDef, PeriodDef, and Constraint nodes are
+ * intermixed in tableElts; constraints, nnconstraints, and periods are NIL.  After
  * parse analysis, tableElts contains just ColumnDefs, nnconstraints contains
- * Constraint nodes of CONSTR_NOTNULL type from various sources, and
- * constraints contains just CONSTR_CHECK Constraint nodes.
+ * Constraint nodes of CONSTR_NOTNULL type from various sources, constraints
+ * contains just CONSTR_CHECK Constraint nodes, and periods contains just
+ * PeriodDefs.
  * ----------------------
  */
 
@@ -2751,6 +2755,7 @@ typedef struct CreateStmt
 	NodeTag		type;
 	RangeVar   *relation;		/* relation to create */
 	List	   *tableElts;		/* column definitions (list of ColumnDef) */
+	List	   *periods;		/* periods (list of PeriodDef nodes) */
 	List	   *inhRelations;	/* relations to inherit from (list of
 								 * RangeVar) */
 	PartitionBoundSpec *partbound;	/* FOR VALUES clause */
@@ -2765,6 +2770,31 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3462,6 +3492,7 @@ typedef struct IndexStmt
index c186a3babd6..364a541e1bd 100644
@@ -2383,6 +2383,8 @@ typedef struct ForPortionOfExpr
index d39e79fa983..d407b4cc436 100644
@@ -339,7 +339,7 @@ PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
index 9f2b58de797..bb40ca56857 100644
 
index 0c655228aef..742e791ebb3 100644
@@ -98,6 +98,8 @@ extern Oid	get_atttype(Oid relid, AttrNumber attnum);
@@ -203,6 +205,7 @@ extern Oid	get_range_subtype(Oid rangeOid);
index 193669f2bc1..9ed429fdadd 100644
@@ -179,6 +179,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
 
index 00000000000..ac11c88e8dc
@@ -0,0 +1,275 @@
+/* Period can't use the same colum for start and end*/
+create table pt (id integer, ds date, de date, period for p (ds, ds));
+ERROR:  column "ds" can't be the start and end column for period "p"
+/* Period can't use the same colum for start and end*/
+alter table pt2 add period for p (ds, ds);
+ERROR:  column "ds" can't be the start and end column for period "p"
index a56df974bfc..aab8ab54dfc 100644
@@ -1160,6 +1160,35 @@ SET SESSION AUTHORIZATION regress_priv_user3;
index 11cc8a01441..feb3001238d 100644

 
@@ -933,6 +1453,91 @@ ERROR:  column "valid_at" is in a primary key
+SELECT attnotnull FROM pg_attribute WHERE attrelid = 'temporal3'::regclass AND attname = 'valid_at';
+ attnotnull 
+------------
+ t
+LINE 1: ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange U...
+-- Same results if we add the PERIOD and PK from ALTER TABLE:
+  ADD CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+SELECT attnotnull FROM pg_attribute WHERE attrelid = 'temporal3'::regclass AND attname = 'valid_at';
+ attnotnull 
+------------
+ t
@@ -1414,32 +2019,236 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;
 
@@ -1467,6 +2276,18 @@ CREATE TABLE temporal_fk_rng2rng (
@@ -3585,83 +4406,3566 @@ SELECT * FROM temporal_fk2_mltrng2mltrng WHERE id = '[200,201)' ORDER BY id, val
 
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: int4range and daterange.
 
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: tsrange and daterange.
+  ('[1,2)', '2018-01-02', '2018-02-03'),
+  ('[1,2)', '2018-03-03', '2018-04-04'),
+  ('[2,3)', '2018-01-01', '2018-01-05'),
+  ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2per SET parent_id = '[8,9)' WHERE id = '[1,2)';
+    ('[5,6)', '2018-01-01', '2018-02-01'),
+    ('[5,6)', '2018-02-01', '2018-03-01');
+    ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+  DELETE FROM temporal_per WHERE id = '[5,6)'; --should not fail yet.
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,6)';
+DELETE FROM temporal_per WHERE id = '[5,6)';
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+  ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+  ('[1,2)', '2018-01-01', '2018-03-01'),
+  ('[1,2)', '2018-03-01', '2018-06-01');
+  ('[1,2)', '2018-01-15', '2018-02-01', '[1,2)'),
+  ('[2,3)', '2018-01-15', '2018-05-01', '[1,2)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-03-01'
+UPDATE temporal_per SET valid_from = '2018-01-01', valid_til = '2018-03-01'
+UPDATE temporal_per SET id = '[2,3)', valid_from = '2018-01-15', valid_til = '2018-03-01'
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_per2per".
+UPDATE temporal_per SET id = '[2,3)'
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_per2per".
+  ('[2,3)', '2018-01-01', '2018-03-01');
+  ('[5,6)', '2018-01-15', '2018-02-01', '[2,3)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-02-15'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+UPDATE temporal_per SET id = '[7,8)'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_per2per WHERE id = '[3,4)';
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,6)';
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_per2per WHERE id = '[3,4)';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)', id2 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,7)', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,9)', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: int4range and daterange.
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: tsrange and daterange.
+  ('[1,2)', '2018-01-02', '2018-02-03'),
+  ('[1,2)', '2018-03-03', '2018-04-04'),
+  ('[2,3)', '2018-01-01', '2018-01-05'),
+  ('[3,4)', '2018-01-01', NULL);
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,2)', '[2018-01-02,2018-02-01)', '[1,2)');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,2)', '[2018-01-02,2018-02-01)', '[1,2)');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-02-20)' WHERE id = '[1,2)';
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-05-01)' WHERE id = '[1,2)';
+UPDATE temporal_fk_rng2per SET parent_id = '[8,9)' WHERE id = '[1,2)';
+    ('[5,6)', '2018-01-01', '2018-02-01'),
+    ('[5,6)', '2018-02-01', '2018-03-01');
+    ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+  DELETE FROM temporal_per WHERE id = '[5,6)'; --should not fail yet.
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,6)';
+DELETE FROM temporal_per WHERE id = '[5,6)';
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+  ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+  ('[1,2)', '2018-01-01', '2018-03-01'),
+  ('[1,2)', '2018-03-01', '2018-06-01');
+  ('[1,2)', '[2018-01-15,2018-02-01)', '[1,2)'),
+  ('[2,3)', '[2018-01-15,2018-05-01)', '[1,2)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-03-01'
+UPDATE temporal_per SET valid_from = '2018-01-01', valid_til = '2018-03-01'
+UPDATE temporal_per SET id = '[2,3)', valid_from = '2018-01-15', valid_til = '2018-03-01'
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2per".
+UPDATE temporal_per SET id = '[2,3)'
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_rng2per".
+  ('[2,3)', '2018-01-01', '2018-03-01');
+  ('[5,6)', '[2018-01-15,2018-02-01)', '[2,3)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-02-15'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+UPDATE temporal_per SET id = '[7,8)'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,4)';
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,6)';
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_rng2per WHERE id = '[3,4)';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+UPDATE temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,8)' WHERE id = '[6,7)';
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+DELETE FROM temporal_per WHERE id = '[6,7)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+UPDATE temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+UPDATE temporal_per2 SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)', id2 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,7)', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,9)', null, null);
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: int4range and daterange.
+DETAIL:  Key columns "valid_at" of the referencing table and "valid_at" of the referenced table are of incompatible types: tsrange and daterange.
+  ('[1,2)', '[2018-01-02,2018-02-03)'),
+  ('[1,2)', '[2018-03-03,2018-04-04)'),
+  ('[2,3)', '[2018-01-01,2018-01-05)'),
+  ('[3,4)', '[2018-01-01,)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', '[2018-02-03,2018-03-03)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2rng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+    ('[5,6)', '[2018-01-01,2018-02-01)'),
+    ('[5,6)', '[2018-02-01,2018-03-01)');
+    ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,6)';
+  ('[5,6)', '[2018-01-01,2018-02-01)'),
+  ('[5,6)', '[2018-02-01,2018-03-01)');
+  ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+WHERE id = '[5,6)' AND valid_at = '[2018-02-01,2018-03-01)';
+  ('[1,2)', '[2018-01-01,2018-03-01)'),
+  ('[1,2)', '[2018-03-01,2018-06-01)');
+  ('[1,2)', '2018-01-15', '2018-02-01', '[1,2)'),
+  ('[2,3)', '2018-01-15', '2018-05-01', '[1,2)');
+UPDATE temporal_rng SET valid_at = '[2018-01-15,2018-03-01)'
+UPDATE temporal_rng SET valid_at = '[2018-01-01,2018-03-01)'
+UPDATE temporal_rng SET id = '[2,3)', valid_at = '[2018-01-15,2018-03-01)'
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_per2rng".
+DETAIL:  Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is still referenced from table "temporal_fk_per2rng".
+  ('[2,3)', '[2018-01-01,2018-03-01)');
+  ('[5,6)', '2018-01-15', '2018-02-01', '[2,3)');
+UPDATE temporal_rng SET valid_at = '[2018-01-15,2018-02-15)'
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,4)';
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', '[2018-01-01,2018-02-01)');
+  ('[5,6)', '[2018-01-01,2018-02-01)'),
+  ('[5,6)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-02-01,2018-03-01)';
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+SELECT * FROM temporal_fk_per2rng WHERE id in ('[3,4)') ORDER BY id, valid_at;
+DELETE FROM temporal_fk_per2rng WHERE id = '[3,4)';
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+ERROR:  unsupported ON DELETE action for foreign key constraint using PERIOD
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', '(,)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
 UPDATE temporal_partitioned_fk_rng2rng SET valid_at = daterange('2000-01-01', '2000-04-01') WHERE id = '[1,2)';
 ERROR:  conflicting key value violates exclusion constraint "tfkp1_pkey"
@@ -4220,4 +8524,18 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 
index 180a8533aab..463c12ef726 100644
@@ -43,7 +43,7 @@ test: copy copyselect copydml copyencoding insert insert_conflict
 
 # ----------

+-- Test with PERIODs
+
 
index 00000000000..b79a9db49ac
@@ -0,0 +1,186 @@
+/* Period can't use the same colum for start and end*/
+create table pt (id integer, ds date, de date, period for p (ds, ds));
+/* Period can't use the same colum for start and end*/
+alter table pt2 add period for p (ds, ds);
index 4658fb8081f..75754fe75c1 100644
@@ -792,6 +792,34 @@ UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2
 
index 224ddef8430..188041e1d07 100644

 
 
+--
@@ -662,6 +972,37 @@ ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL;
+SELECT attnotnull FROM pg_attribute WHERE attrelid = 'temporal3'::regclass AND attname = 'valid_at';
+-- Same results if we add the PERIOD and PK from ALTER TABLE:
+  ADD CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+SELECT attnotnull FROM pg_attribute WHERE attrelid = 'temporal3'::regclass AND attname = 'valid_at';
 
 --
@@ -963,6 +1304,121 @@ SELECT * FROM temporal_mltrng3 ORDER BY id, valid_at;

+--
+-- ON CONFLICT: PERIODs
+--
+
+TRUNCATE temporal_per;
@@ -986,6 +1442,29 @@ ALTER TABLE temporal3 DROP COLUMN valid_at CASCADE;
 
@@ -1016,6 +1495,7 @@ CREATE TABLE temporal_fk_rng2rng (
 
 -- with mismatched PERIOD columns:
@@ -2567,47 +3047,2555 @@ INSERT INTO temporal_fk2_mltrng2mltrng (id, valid_at, parent_id1, parent_id2) VA
 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;

 

---
 
-CREATE TABLE temporal_partitioned_rng (
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
   id int4range,
-  valid_at daterange,
-  name text,
-  CONSTRAINT temporal_paritioned_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)', '[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'),
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_per2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_per2;
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2per
+DROP TABLE temporal_fk2_per2per;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2per
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2per
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_per2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,2)', '2018-01-02', '2018-02-03'),
+  ('[1,2)', '2018-03-03', '2018-04-04'),
+  ('[2,3)', '2018-01-01', '2018-01-05'),
+  ('[3,4)', '2018-01-01', NULL);
+
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- okay again:
+DELETE FROM temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2per_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_per2per SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2per SET parent_id = '[8,9)' WHERE id = '[1,2)';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,6)', '2018-01-01', '2018-02-01'),
+    ('[5,6)', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,6)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,2)', '2018-01-01', '2018-03-01'),
+  ('[1,2)', '2018-03-01', '2018-06-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[1,2)', '2018-01-15', '2018-02-01', '[1,2)'),
+  ('[2,3)', '2018-01-15', '2018-05-01', '[1,2)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_per SET valid_from = '2018-01-01', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_per SET id = '[2,3)', valid_from = '2018-01-15', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_per SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[2,3)', '2018-01-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES
+  ('[5,6)', '2018-01-15', '2018-02-01', '[2,3)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-02-15'
+WHERE id = '[2,3)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,8)'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+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_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,8)'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per 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_per2per WHERE id = '[3,4)';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2per
+    ALTER CONSTRAINT temporal_fk_per2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,6)';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_per2per 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_per2per WHERE id = '[3,4)';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk;
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+
+--
+-- per2per 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_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2per
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2per
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2per_fk,
+  ADD CONSTRAINT temporal_fk_per2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_per2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_per2per (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)', id2 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_per2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,7)', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_per2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2per_fk,
+  ADD CONSTRAINT temporal_fk2_per2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,9)', null, null);
+INSERT INTO temporal_fk2_per2per (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '2018-01-01', '2021-01-01', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_per2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FOREIGN KEY, range references PERIOD
+--
+
+-- test table setup
+DROP TABLE temporal_per CASCADE;
+CREATE TABLE temporal_per (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til)
+);
+ALTER TABLE temporal_per
+  ADD CONSTRAINT temporal_per_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at int4range,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+
+-- works: PERIOD for referenced, range for referencing
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+\d temporal_fk_rng2per
+DROP TABLE temporal_fk_rng2per;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_per
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+);
+DROP TABLE temporal_fk_rng2per;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_per
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_rng2per_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_per (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_per2 CASCADE;
+CREATE TABLE temporal_per2 (
+  id1 int4range,
+  id2 int4range,
+  valid_from date,
+  valid_til date,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_per2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_rng2per_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_rng2per
+DROP TABLE temporal_fk2_rng2per;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id int4range,
+  CONSTRAINT temporal_fk_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_rng2per (
+  id int4range,
+  valid_at daterange,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  CONSTRAINT temporal_fk2_rng2per_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_rng2per
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_per2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_rng2per
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ALTER COLUMN valid_at TYPE tsrange USING tsrange(lower(valid_at), upper(valid_at));
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN valid_at TYPE daterange USING daterange(lower(valid_at)::date, upper(valid_at)::date);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_per (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_rng2per;
+DELETE FROM temporal_per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,2)', '2018-01-02', '2018-02-03'),
+  ('[1,2)', '2018-03-03', '2018-04-04'),
+  ('[2,3)', '2018-01-01', '2018-01-05'),
+  ('[3,4)', '2018-01-01', NULL);
+
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,2)', '[2018-01-02,2018-02-01)', '[1,2)');
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- okay again:
+DELETE FROM temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2per_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[1,2)', '[2018-01-02,2018-02-01)', '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+-- now it should work:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[1,2)', '2018-02-03', '2018-03-03');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[2,3)', '[2018-01-02,2018-04-01)', '[1,2)');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-02-20)' WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_rng2per SET valid_at = '[2018-01-02,2018-05-01)' WHERE id = '[1,2)';
+UPDATE temporal_fk_rng2per SET parent_id = '[8,9)' WHERE id = '[1,2)';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+    ('[5,6)', '2018-01-01', '2018-02-01'),
+    ('[5,6)', '2018-02-01', '2018-03-01');
+  INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+    ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_per WHERE id = '[5,6)';
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+UPDATE temporal_per SET valid_from = '2016-02-01', valid_til = '2016-03-01'
+WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[6,7)', '2018-01-01', '2018-02-01'),
+  ('[6,7)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[4,5)', '[2018-01-15,2018-02-15)', '[6,7)');
+UPDATE temporal_per
+SET valid_from = CASE WHEN valid_from = '2018-01-01' THEN '2018-01-01'
+                      WHEN valid_from = '2018-02-01' THEN '2018-01-05' END::date,
+    valid_til =  CASE WHEN valid_from = '2018-01-01' THEN '2018-01-05'
+                      WHEN valid_from = '2018-02-01' THEN '2018-03-01' END::date
+WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[1,2)', '2018-01-01', '2018-03-01'),
+  ('[1,2)', '2018-03-01', '2018-06-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[1,2)', '[2018-01-15,2018-02-01)', '[1,2)'),
+  ('[2,3)', '[2018-01-15,2018-05-01)', '[1,2)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_per SET valid_from = '2018-01-01', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_per SET id = '[2,3)', valid_from = '2018-01-15', valid_til = '2018-03-01'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_per SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[2,3)', '2018-01-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES
+  ('[5,6)', '[2018-01-15,2018-02-01)', '[2,3)');
+UPDATE temporal_per SET valid_from = '2018-01-15', valid_til = '2018-02-15'
+WHERE id = '[2,3)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_per SET id = '[7,8)'
+WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- changing an unreferenced part is okay:
+UPDATE temporal_per
+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_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+SET id = '[7,8)'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per 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_rng2per WHERE id = '[3,4)';
+UPDATE temporal_per SET valid_from = '2016-01-01', valid_til = '2016-02-01'
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON UPDATE RESTRICT;
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[5,6)', '2018-01-01', '2018-02-01');
+DELETE FROM temporal_per WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES
+  ('[5,6)', '2018-01-01', '2018-02-01'),
+  ('[5,6)', '2018-02-01', '2018-03-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[3,4)', '[2018-01-05,2018-01-10)', '[5,6)');
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_rng2per
+    ALTER CONSTRAINT temporal_fk_rng2per_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- deleting an unreferenced part is okay:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03'
+WHERE id = '[5,6)';
+-- deleting just a part fails:
+DELETE FROM temporal_per
+FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
+WHERE id = '[5,6)';
+SELECT * FROM temporal_per WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at;
+SELECT * FROM temporal_fk_rng2per 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_rng2per WHERE id = '[3,4)';
+DELETE FROM temporal_per WHERE id = '[5,6)' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk;
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_per
+  ON DELETE RESTRICT;
+
+--
+-- rng2per 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_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+ALTER TABLE temporal_fk_rng2per
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+ALTER TABLE temporal_fk_rng2per
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+ALTER TABLE temporal_fk_rng2per
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_rng2per_fk,
+  ADD CONSTRAINT temporal_fk_rng2per_fk
+    FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_per
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+UPDATE temporal_per SET id = '[9,10)' WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_per, temporal_fk_rng2per;
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[-1,-1]', null, null);
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per (id, valid_from, valid_til) VALUES ('[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk_rng2per (id, valid_at, parent_id) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)');
+DELETE FROM temporal_per WHERE id = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE CASCADE ON UPDATE CASCADE;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL ON UPDATE SET NULL;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+ALTER TABLE temporal_fk2_rng2per
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET NULL (parent_id1) ON UPDATE SET NULL;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+UPDATE temporal_per2 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_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_per2 SET id1 = '[7,8)', id2 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+UPDATE temporal_per2 SET id1 = '[9,10)', id2 = '[9,10)' WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[-1,-1]', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_per2, temporal_fk2_rng2per;
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[6,7)', null, null);
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[6,7)', '[6,7)', '2018-01-01', '2021-01-01');
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[100,100]', '[2018-01-01,2021-01-01)', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:
+ALTER TABLE temporal_fk2_rng2per
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_rng2per_fk,
+  ADD CONSTRAINT temporal_fk2_rng2per_fk
+    FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_per2
+    ON DELETE SET DEFAULT (parent_id1) ON UPDATE SET DEFAULT;
+-- leftovers on both sides:
+DELETE FROM temporal_per2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_per2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2018-01-01', '2020-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[8,9)', '[8,9)', '2020-01-01', '2021-01-01');
+INSERT INTO temporal_per2 (id1, id2, valid_from, valid_til) VALUES ('[-1,-1]', '[8,9)', null, null);
+INSERT INTO temporal_fk2_rng2per (id, valid_at, parent_id1, parent_id2) VALUES ('[200,200]', '[2018-01-01,2021-01-01)', '[8,9)', '[8,9)');
+DELETE FROM temporal_per2 WHERE id1 = '[8,9)' AND valid_at @> '2019-01-01'::date;
+SELECT * FROM temporal_fk2_rng2per WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FOREIGN KEY, PERIOD references range
+--
+
+-- test table setup
+DROP TABLE temporal_rng CASCADE;
+CREATE TABLE temporal_rng (
+  id int4range,
+  valid_at daterange
+);
+ALTER TABLE temporal_rng
+  ADD CONSTRAINT temporal_rng_pk
+  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+-- Can't create a FK with a mismatched range type
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from int,
+  valid_til int,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+
+-- works: range for referenced, PERIOD for referencing
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+\d temporal_fk_per2rng
+DROP TABLE temporal_fk_per2rng;
+
+-- with mismatched PERIOD columns:
+
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+-- REFERENCES part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, valid_at)
+-- both should specify PERIOD:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, valid_at)
+);
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- (parent_id, valid_at) REFERENCES [implicit]
+-- FOREIGN KEY part should specify PERIOD
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, valid_at)
+    REFERENCES temporal_rng
+);
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng (id)
+);
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng (id, PERIOD valid_at)
+);
+-- with inferred PK on the referenced table:
+-- (parent_id, PERIOD valid_at) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+    REFERENCES temporal_rng
+);
+DROP TABLE temporal_fk_per2rng;
+-- (parent_id) REFERENCES [implicit]
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id)
+    REFERENCES temporal_rng
+);
+
+-- should fail because of duplicate referenced columns:
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk_per2rng_fk FOREIGN KEY (parent_id, PERIOD parent_id)
+    REFERENCES temporal_rng (id, PERIOD id)
+);
+
+-- Two scalar columns
+DROP TABLE temporal_rng2 CASCADE;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at daterange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+  CONSTRAINT temporal_fk2_per2rng_fk FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+    REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at)
+);
+\d temporal_fk2_per2rng
+DROP TABLE temporal_fk2_per2rng;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+CREATE TABLE temporal_fk_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng (id, PERIOD valid_at);
+-- Two scalar columns:
+CREATE TABLE temporal_fk2_per2rng (
+  id int4range,
+  valid_from date,
+  valid_til date,
+  parent_id1 int4range,
+  parent_id2 int4range,
+  PERIOD FOR valid_at (valid_from, valid_til),
+  CONSTRAINT temporal_fk2_per2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_fk2_per2rng
+  ADD CONSTRAINT temporal_fk2_per2rng_fk
+  FOREIGN KEY (parent_id1, parent_id2, PERIOD valid_at)
+  REFERENCES temporal_rng2 (id1, id2, PERIOD valid_at);
+\d temporal_fk2_per2rng
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE timestamp,
+  ALTER COLUMN valid_til TYPE timestamp,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP PERIOD FOR valid_at,
+  ALTER COLUMN valid_from TYPE date,
+  ALTER COLUMN valid_til TYPE date,
+  ADD PERIOD FOR valid_at (valid_from, valid_til);
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+
+-- should fail because of duplicate referenced columns:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk2
+  FOREIGN KEY (parent_id, PERIOD parent_id)
+  REFERENCES temporal_rng (id, PERIOD id);
+
+--
+-- test with rows already
+--
+
+DELETE FROM temporal_fk_per2rng;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', '[2018-01-02,2018-02-03)'),
+  ('[1,2)', '[2018-03-03,2018-04-04)'),
+  ('[2,3)', '[2018-01-01,2018-01-05)'),
+  ('[3,4)', '[2018-01-01,)');
+
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+-- should fail:
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- okay again:
+DELETE FROM temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_per2rng_fk';
+
+--
+-- test FK referencing inserts
+--
+
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[1,2)', '2018-01-02', '2018-02-01', '[1,2)');
+-- should fail:
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+-- now it should work:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[1,2)', '[2018-02-03,2018-03-03)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[2,3)', '2018-01-02', '2018-04-01', '[1,2)');
+
+--
+-- test FK referencing updates
+--
+
+-- slide the edge across a referenced transition:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-02-20' WHERE id = '[1,2)';
+-- should fail:
+UPDATE temporal_fk_per2rng SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,2)';
+UPDATE temporal_fk_per2rng SET parent_id = '[8,9)' WHERE id = '[1,2)';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_rng (id, valid_at) VALUES
+    ('[5,6)', '[2018-01-01,2018-02-01)'),
+    ('[5,6)', '[2018-02-01,2018-03-01)');
+  INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+    ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_rng WHERE id = '[5,6)'; --should not fail yet.
+COMMIT; -- should fail here.
+
+--
+-- test FK referenced updates NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE NO ACTION;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', '[2018-01-01,2018-02-01)');
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)' WHERE id = '[5,6)';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', '[2018-01-01,2018-02-01)'),
+  ('[5,6)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+UPDATE temporal_rng SET valid_at = '[2016-02-01,2016-03-01)'
+WHERE id = '[5,6)' AND valid_at = '[2018-02-01,2018-03-01)';
+-- A PK update sliding the edge between two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[6,7)', '[2018-01-01,2018-02-01)'),
+  ('[6,7)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[4,5)', '2018-01-15', '2018-02-15', '[6,7)');
+UPDATE temporal_rng
+SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05')
+                    WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END
+WHERE id = '[6,7)';
+-- a PK update shrinking the referenced range but still valid:
+-- There are two references: one fulfilled by the first pk row,
+-- the other fulfilled by both pk rows combined.
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[1,2)', '[2018-01-01,2018-03-01)'),
+  ('[1,2)', '[2018-03-01,2018-06-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[1,2)', '2018-01-15', '2018-02-01', '[1,2)'),
+  ('[2,3)', '2018-01-15', '2018-05-01', '[1,2)');
+UPDATE temporal_rng SET valid_at = '[2018-01-15,2018-03-01)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update growing the referenced range is fine:
+UPDATE temporal_rng SET valid_at = '[2018-01-01,2018-03-01)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date;
+-- a PK update shrinking the referenced range and changing the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)', valid_at = '[2018-01-15,2018-03-01)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update changing only the id invalidates the whole range:
+UPDATE temporal_rng SET id = '[2,3)'
+WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date;
+-- a PK update that loses time from both ends, but is still valid:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[2,3)', '[2018-01-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES
+  ('[5,6)', '2018-01-15', '2018-02-01', '[2,3)');
+UPDATE temporal_rng SET valid_at = '[2018-01-15,2018-02-15)'
+WHERE id = '[2,3)';
+-- a PK update that fails because both are referenced:
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+-- a PK update that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+  WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01');
+COMMIT;
+-- changing the scalar part fails:
+UPDATE temporal_rng SET id = '[7,8)'
+WHERE id = '[5,6)' AND valid_at = '[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_per2rng 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_per2rng WHERE id = '[3,4)';
+UPDATE temporal_rng SET valid_at = '[2016-01-01,2016-02-01)'
+WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced updates RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON UPDATE RESTRICT;
+
+--
+-- test FK referenced deletes NO ACTION
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', '[2018-01-01,2018-02-01)');
+DELETE FROM temporal_rng WHERE id = '[5,6)';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_rng (id, valid_at) VALUES
+  ('[5,6)', '[2018-01-01,2018-02-01)'),
+  ('[5,6)', '[2018-02-01,2018-03-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[3,4)', '2018-01-05', '2018-01-10', '[5,6)');
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-02-01,2018-03-01)';
+-- a PK delete that fails because both are referenced:
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+-- a PK delete that fails because both are referenced, but not 'til commit:
+BEGIN;
+  ALTER TABLE temporal_fk_per2rng
+    ALTER CONSTRAINT temporal_fk_per2rng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  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)';
+-- 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_per2rng 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_per2rng WHERE id = '[3,4)';
+DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = '[2018-01-01,2018-02-01)';
+
+--
+-- test FK referenced deletes RESTRICT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk;
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+  FOREIGN KEY (parent_id, PERIOD valid_at)
+  REFERENCES temporal_rng
+  ON DELETE RESTRICT;
+
+--
+-- per2rng 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_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2rng
+  ADD CONSTRAINT temporal_fk_per2rng_fk
+    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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2rng
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)');
+ALTER TABLE temporal_fk_per2rng
+  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk_per2rng_fk,
+  ADD CONSTRAINT temporal_fk_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT
+--
+
+TRUNCATE temporal_rng, temporal_fk_per2rng;
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', '(,)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng WHERE id = '[6,7)';
+SELECT * FROM temporal_fk_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng (id, valid_at) VALUES ('[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk_per2rng (id, valid_from, valid_til, parent_id) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes CASCADE (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+UPDATE temporal_rng2 SET id1 = '[7,8)' WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET NULL (two scalar cols, SET NULL subset)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO delete:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced updates SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  ALTER COLUMN parent_id2 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng WHERE id = '[100,100]' 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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[-1,-1]', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' ORDER BY id, valid_at;
+
+--
+-- test FK referenced deletes SET DEFAULT (two scalar cols, SET DEFAULT subset)
+--
+
+TRUNCATE temporal_rng2, temporal_fk2_per2rng;
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[6,7)', '(,)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[6,7)', '[6,7)', '[2018-01-01,2021-01-01)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[100,100]', '2018-01-01', '2021-01-01', '[6,7)', '[6,7)');
+-- fails because you can't set the PERIOD column:
+ALTER TABLE temporal_fk2_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng
+  ALTER COLUMN parent_id1 SET DEFAULT '[-1,-1]',
+  DROP CONSTRAINT temporal_fk2_per2rng_fk,
+  ADD CONSTRAINT temporal_fk2_per2rng_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_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- non-FPO update:
+DELETE FROM temporal_rng2 WHERE id1 = '[6,7)';
+SELECT * FROM temporal_fk2_per2rng WHERE id = '[100,100]' ORDER BY id, valid_at;
+-- FK across two referenced rows:
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2018-01-01,2020-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[8,9)', '[8,9)', '[2020-01-01,2021-01-01)');
+INSERT INTO temporal_rng2 (id1, id2, valid_at) VALUES ('[-1,-1]', '[8,9)', '(,)');
+INSERT INTO temporal_fk2_per2rng (id, valid_from, valid_til, parent_id1, parent_id2) VALUES ('[200,200]', '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_per2rng WHERE id = '[200,200]' 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
+--
+
+CREATE TABLE temporal_partitioned_rng (
+  id int4range,
+  valid_at daterange,
+  name text,
+  CONSTRAINT temporal_paritioned_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)', '[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'),
   ('[2,3)', daterange('2000-01-01', '2010-01-01'), 'two');
 
 CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -3007,4 +5995,19 @@ SELECT * FROM temporal_partitioned_fk_mltrng2mltrng WHERE id = '[15,16)';
 DROP TABLE temporal_partitioned_fk_mltrng2mltrng;
 DROP TABLE temporal_partitioned_mltrng;
 
+--
2.39.5

---

From: Paul Jungwirth <[email-masked]>
Date: Wed, 19 Mar 2025 20:33:36 -0700

On 3/3/25 02:05, Peter Eisentraut wrote:
 > In the theory of the SQL standard, executing referential actions and checking the foreign-key
 > constraint are two separate steps.  So it kind of goes like this:
 >
 > 1. run command
 > 2. run any referential actions
 > 3. check that foreign key is still satisfied
 >
 > This is why the default referential action is called "NO ACTION": It just skips the step 2.  But it
 > still does step 3.
 >
 > This means that under RESTRICT and with my interpretation, the check for a RESTRICT violation in
 > step 2 can "ignore" the period part, but the step 3 still has to observe the period part.
 >
 > In the implementation, these steps are mostly combined into one trigger function, so it might be a
 > bit tricky to untangle them.

I understand that there are those separate steps. But it still doesn't make sense for RESTRICT to 
ignore the temporal part of the key. Actually, talking about "actions" reminds me of another reason: 
the effect of CASCASE/SET NULL/SET DEFAULT actions should also be limited to only the part of 
history that was updated/deleted in the referenced row. (This is why their implementation depends on 
FOR PORTION OF.) Otherwise a temporal CASCADE/SET NULL/SET DEFAULT would wreck havoc on your data. 
But if that's how these other actions behave, shouldn't RESTRICT behave the same way? Again, it's 
not clear why anyone would want a temporal foreign key that ignores its temporal attribute. And as I 
explained before, I don't think that's what a careful read of the standard says.

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Thu, 8 May 2025 09:20:06 -0700

> Here is another set of patches, just rebasing and fixing a CI failure in contrib/sepgsql.

v51 attached, just rebasing to b560ce7884.

Yours,

---

From: Robert Haas <[email-masked]>
Date: Fri, 9 May 2025 14:49:57 -0400

> v51 attached, just rebasing to b560ce7884.

I think these patches would benefit from some work to make them more
understandable for people who don't already know what they're intended
to accomplish. I suspect that's actually a prerequisite to committing
them. I started by opening up v51-0001, and the commit message told me
this:

But there is no such thing as FOR PORTION OF just yet, so this is a
forward reference. I had to open v51-0002 to try to figure it out.
Ideally, concepts get introduced before they're used. The rest of the
commit message for v51-0001 reads like this:

For someone familiar with this work, this probably makes sense, but
I'm not and it doesn't. I don't even understand to what "work like
minus but don't fail on splits" is intended to refer. I mean, is there
some existing minus thing? I don't see it among the GIST_*_PROC
symbols, and I don't know where else I'd go looking.

As I read through the documentation changes, I find that you use terms
like "leftovers" or "leftover row" in various parts of the
documentation that are quite far away from and not obviously linked to
the documentation of FOR PORTION OF. I think that's going to be very
confusing. In other places where we used specialized terms like this,
we often using <xref> or <link> to reference the place where the term
is defined. You don't really quite have a place where that happens,
though, although maybe linking to the documentation of FOR PORTION OF
would be good enough.

The documentation of FOR PORTION OF makes more sense to me for range
types than for multirange types. I gather that if I have a range like
[1,10] and I deleted from 3 to 6, I'm going to end up with two ranges.
I'm not sure whether 3 or 6 are inclusive or exclusive bounds, so I
don't know if I should expect to end up with [1,3) and (6,10] or [1,3]
and [6,10], and I kind of wonder if I should get to say which one I
want, but anyway now I have two records, my original one having been
split. But if [1,10] is a multirange, it's unnecessary to split the
record in two in order to accommodate a deletion: i can just include
both sub-ranges in the original value. But the documentation doesn't
seem to mention this one way or the other: will I still (needlessly?)
create two records, or will I just update the one record I already and
split the range? Since the documentation is already talking about a
feature specific to range and multirange types, it seems like this
kind of stuff should be mentioned.

Hmm. I guess this implies that we never do an update -- it's always a
DELETE followed by zero, one, or two INSERTs. That seems like it's
leaving quite a bit of efficiency on the table, because updating an
existing row in place could potentially be a HOT update.

Is this feature as per the SQL standard? And, assuming yes, does the
SQL standard specify that permission checks should work as you
describe here, or is that something we decided?

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 21 May 2025 09:28:07 -0700

Thank you for the review! I think we covered everything here at the
PgConf.dev Advanced Patch Feedback session, but for the sake of the
list I'll reply to the specifics. I'll send a longer email summarizing
more from that session later, probably in a few days.

On Fri, May 9, 2025 at 2:50 PM Robert Haas <[email-masked]> wrote:
> to accomplish.

I'll improve the commit messages in the next version of the patches. I
agree they have been pretty brief. Also we agreed that there should be
a separate documentation chapter/section introducing the concepts
behind temporal tables and application time. That will help in
general, plus it will give the existing doc changes a place to
reference for new terms.

> The rest of the

Yes, I think I will rename these functions to
{,multi}range_minus_multi. I'm trying to say that they work like
range_minus (typically used via the minus operator). But range_minus
raises an exception if you subtract from the middle of its first
parameter/operand, because the result would yield two separate ranges.
range_minus_multi is a set-returning function to avoid that.

You can't return an array of ranges, because where our polymorphic
type system sees an input parameter of anyrange with a return type of
anyarray, it treats them as range<T> -> array<T>, not range<T> ->
array<range<T>>.

I don't want to return a multirange here either because that design
locks us in to range types, and I'd rather leave the door open to
support arbitrary user-defined types. A SRF means the type can bring
its own function to generate as many "leftovers" as it needs.

Also it sounds like an opclass support proc is not needed here at all.
There is no index involved (or there could be several). Perhaps a type
support proc. But for now we can support just range & multirange with
neither.

> though, although maybe linking to the documentation of FOR PORTION OF

Okay, see above. We also discussed using a term that better signals
its context, like "temporal range leftovers". I think I like just
"temporal leftovers" better, or maybe "temporal update leftovers" or
"temporal delete leftovers", depending on the operation.

> types than for multirange types.

Okay, this is good feedback. I'll work on it!

> I gather that if I have a range like

A multirange always returns just 0 or 1 result from without_portion
aka multirange_minus_multi. As you say, 2 results are unneeded.

> describe here, or is that something we decided?

This is not quite right. We never delete then insert the existing row
(except in the usual MVCC sense). The standard is very clear here: a
temporal update always updates the existing row (and automatically
changes its start/end time to not extend beyond the targeted range),
and a temporal delete always deletes the existing row. But if the
row's start/end times extended beyond just the targeted range, there
is still history your update/delete wasn't supposed to touch. So the
update/delete should be followed by 0, 1, or 2 inserts to preserve the
row's original values for those portions of history. I'll include this
in the docs I write.

Where the standard is not very clear is how to fire triggers for the
inserts. I'll leave those details for the next email.

The standard doesn't say anything about permissions either. Personally
I feel that since the inserts aren't adding any history that wasn't
there already, we shouldn't require insert permission. Also if we did
require insert permission, there would be no way to grant someone
access to correct existing history without adding new history. But I
don't have a strong opinion here. As my comment mentioned, perhaps
there is a security flaw, since skipping the insert permission check
means insert triggers fire for unauthorized users. Whatever we decide,
permissions and triggers should ideally cooperate to yield a
consistent mental model.

For that next email I'll start a separate thread, since this one has
gotten so long. Also I think this project is overdue for a wiki page.

Yours,

--
Paul              ~{:-)
[email-masked]

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 26 May 2025 07:57:19 +0200

On 17.09.24 11:45, Peter Eisentraut wrote:
>> I have studied your patches v39-0001 through v39-0004, which 
>> correspond to what had been reverted plus the new empty range check 
>> plus various minor fixes.  This looks good to me now, so I propose to 
>> go ahead with that.
>>

Here we added a gist support function that we internally refer to by the 
symbol GIST_STRATNUM_PROC.  This translated from "well-known" strategy 
numbers to opfamily-specific strategy numbers.  However, we later 
changed this to fit into index-AM-level compare type mapping, so this 
function actually now maps from compare type to opfamily-specific 
strategy numbers.  So I'm wondering if this name is still good.

Moreover, the index AM level also supports the opposite, a function to 
map from strategy number to compare type.  This is currently not 
supported in gist, but one might wonder what this function is supposed 
to be called when it is added.

So I went through and updated the naming of the gist-level functionality 
to be more in line with the index-AM-level functionality; see attached 
patch.  I think this makes sense because these are essentially the same 
thing on different levels.  What do you think?  (This would be for PG18.)
From 5cada8bb5032e7b2ee6e218dc38bc6ff35096a23 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Mon, 26 May 2025 07:48:51 +0200
Subject: [PATCH] WIP: Rename gist stratnum support function

---
 contrib/btree_gist/btree_gist--1.7--1.8.sql  | 54 ++++++++++----------
 contrib/btree_gist/btree_gist.c              |  4 +-
 contrib/btree_gist/expected/stratnum.out     | 18 +++----
 contrib/btree_gist/sql/stratnum.sql          |  6 +--
 doc/src/sgml/gist.sgml                       | 25 ++++++---
 doc/src/sgml/xindex.sgml                     |  2 +-
 src/backend/access/gist/gistutil.c           | 14 ++---
 src/backend/access/gist/gistvalidate.c       |  6 +--
 src/include/access/gist.h                    |  2 +-
 src/include/catalog/pg_amproc.dat            | 12 ++---
 src/include/catalog/pg_proc.dat              |  4 +-
 src/test/regress/expected/misc_functions.out | 18 +++----
 src/test/regress/sql/misc_functions.sql      |  6 +--
 13 files changed, 91 insertions(+), 80 deletions(-)

diff --git a/contrib/btree_gist/btree_gist--1.7--1.8.sql b/contrib/btree_gist/btree_gist--1.7--1.8.sql
index 4ff9c43a8eb..8f79365a461 100644
--- a/contrib/btree_gist/btree_gist--1.7--1.8.sql
+++ b/contrib/btree_gist/btree_gist--1.7--1.8.sql
@@ -3,85 +3,85 @@
 -- complain if script is sourced in psql, rather than via CREATE EXTENSION
 \echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.8'" to load this file. \quit
 
-CREATE FUNCTION gist_stratnum_btree(int)
+CREATE FUNCTION gist_translate_cmptype_btree(int)
 RETURNS smallint
 AS 'MODULE_PATHNAME'
 LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
 
 ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_time_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_date_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_text_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
 
 ALTER OPERATOR FAMILY gist_bool_ops USING gist ADD
-	FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ;
+	FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ;
diff --git a/contrib/btree_gist/btree_gist.c b/contrib/btree_gist/btree_gist.c
index 280ce808456..39fcbdad334 100644
--- a/contrib/btree_gist/btree_gist.c
+++ b/contrib/btree_gist/btree_gist.c
@@ -15,7 +15,7 @@ PG_MODULE_MAGIC_EXT(
 PG_FUNCTION_INFO_V1(gbt_decompress);
 PG_FUNCTION_INFO_V1(gbtreekey_in);
 PG_FUNCTION_INFO_V1(gbtreekey_out);
-PG_FUNCTION_INFO_V1(gist_stratnum_btree);
+PG_FUNCTION_INFO_V1(gist_translate_cmptype_btree);
 
 /**************************************************
  * In/Out for keys
@@ -62,7 +62,7 @@ gbt_decompress(PG_FUNCTION_ARGS)
  * Returns the btree number for supported operators, otherwise invalid.
  */
 Datum
-gist_stratnum_btree(PG_FUNCTION_ARGS)
+gist_translate_cmptype_btree(PG_FUNCTION_ARGS)
 {
 	CompareType cmptype = PG_GETARG_INT32(0);
 
diff --git a/contrib/btree_gist/expected/stratnum.out b/contrib/btree_gist/expected/stratnum.out
index dd0edaf4a20..8222b661538 100644
--- a/contrib/btree_gist/expected/stratnum.out
+++ b/contrib/btree_gist/expected/stratnum.out
@@ -1,13 +1,13 @@
--- test stratnum support func
-SELECT gist_stratnum_btree(7);
- gist_stratnum_btree 
----------------------
-                   0
+-- test stratnum translation support func
+SELECT gist_translate_cmptype_btree(7);
+ gist_translate_cmptype_btree 
+------------------------------
+                            0
 (1 row)
 
-SELECT gist_stratnum_btree(3);
- gist_stratnum_btree 
----------------------
-                   3
+SELECT gist_translate_cmptype_btree(3);
+ gist_translate_cmptype_btree 
+------------------------------
+                            3
 (1 row)
 
diff --git a/contrib/btree_gist/sql/stratnum.sql b/contrib/btree_gist/sql/stratnum.sql
index 75adddad849..da8bbf883b0 100644
--- a/contrib/btree_gist/sql/stratnum.sql
+++ b/contrib/btree_gist/sql/stratnum.sql
@@ -1,3 +1,3 @@
--- test stratnum support func
-SELECT gist_stratnum_btree(7);
-SELECT gist_stratnum_btree(3);
+-- test stratnum translation support func
+SELECT gist_translate_cmptype_btree(7);
+SELECT gist_translate_cmptype_btree(3);
diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index a373a8aa4b2..ee86e170055 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -1170,7 +1170,7 @@ <title>Extensibility</title>
     </varlistentry>
 
     <varlistentry>
-     <term><function>stratnum</function></term>
+     <term><function>translate_cmptype</function></term>
      <listitem>
       <para>
        Given a <literal>CompareType</literal> value from
@@ -1188,12 +1188,23 @@ <title>Extensibility</title>
        non-<literal>WITHOUT OVERLAPS</literal> part(s) of an index constraint.
       </para>
 
+      <para>
+       This support function corresponds to the index access method callback
+       function <structfield>amtranslatecmptype</structfield> (see <xref
+       linkend="index-functions"/>).  The
+       <structfield>amtranslatecmptype</structfield> callback function for
+       GiST indexes merely calls down to the
+       <function>translate_cmptype</function> support function of the
+       respective operator family, since the GiST index access method has no
+       fixed strategy numbers itself.
+      </para>
+
       <para>
        The <acronym>SQL</acronym> declaration of the function must look like
        this:
 
 <programlisting>
-CREATE OR REPLACE FUNCTION my_stratnum(integer)
+CREATE OR REPLACE FUNCTION my_translate_cmptype(integer)
 RETURNS smallint
 AS 'MODULE_PATHNAME'
 LANGUAGE C STRICT;
@@ -1202,7 +1213,7 @@ <title>Extensibility</title>
        And the operator family registration must look like this:
 <programlisting>
 ALTER OPERATOR FAMILY my_opfamily USING gist ADD
-    FUNCTION 12 ("any", "any") my_stratnum(int);
+    FUNCTION 12 ("any", "any") my_translate_cmptype(int);
 </programlisting>
       </para>
 
@@ -1210,10 +1221,10 @@ <title>Extensibility</title>
         The matching code in the C module could then follow this skeleton:
 
 <programlisting>
-PG_FUNCTION_INFO_V1(my_stratnum);
+PG_FUNCTION_INFO_V1(my_translate_cmptype);
 
 Datum
-my_stratnum(PG_FUNCTION_ARGS)
+my_translate_cmptype(PG_FUNCTION_ARGS)
 {
     CompareType cmptype = PG_GETARG_INT32(0);
     StrategyNumber ret = InvalidStrategy;
@@ -1232,11 +1243,11 @@ <title>Extensibility</title>
       <para>
        One translation function is provided by
        <productname>PostgreSQL</productname>:
-       <literal>gist_stratnum_common</literal> is for operator classes that
+       <literal>gist_translate_cmptype_common</literal> is for operator classes that
        use the <literal>RT*StrategyNumber</literal> constants.
        The <literal>btree_gist</literal>
        extension defines a second translation function,
-       <literal>gist_stratnum_btree</literal>, for operator classes that use
+       <literal>gist_translate_cmptype_btree</literal>, for operator classes that use
        the <literal>BT*StrategyNumber</literal> constants.
       </para>
      </listitem>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 7e23a7b6e43..3d315df2f98 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -598,7 +598,7 @@ <title>GiST Support Functions</title>
        <entry>11</entry>
       </row>
       <row>
-       <entry><function>stratnum</function></entry>
+       <entry><function>translate_cmptype</function></entry>
        <entry>translate compare types to strategy numbers
         used by the operator class (optional)</entry>
        <entry>12</entry>
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index a6b701943d3..c0aa7d0222f 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1058,11 +1058,11 @@ gistGetFakeLSN(Relation rel)
 }
 
 /*
- * This is a stratnum support function for GiST opclasses that use the
- * RT*StrategyNumber constants.
+ * This is a stratnum translation support function for GiST opclasses that use
+ * the RT*StrategyNumber constants.
  */
 Datum
-gist_stratnum_common(PG_FUNCTION_ARGS)
+gist_translate_cmptype_common(PG_FUNCTION_ARGS)
 {
 	CompareType cmptype = PG_GETARG_INT32(0);
 
@@ -1090,9 +1090,9 @@ gist_stratnum_common(PG_FUNCTION_ARGS)
 /*
  * Returns the opclass's private stratnum used for the given compare type.
  *
- * Calls the opclass's GIST_STRATNUM_PROC support function, if any,
- * and returns the result.
- * Returns InvalidStrategy if the function is not defined.
+ * Calls the opclass's GIST_TRANSLATE_CMPTYPE_PROC support function, if any,
+ * and returns the result.  Returns InvalidStrategy if the function is not
+ * defined.
  */
 StrategyNumber
 gisttranslatecmptype(CompareType cmptype, Oid opfamily)
@@ -1101,7 +1101,7 @@ gisttranslatecmptype(CompareType cmptype, Oid opfamily)
 	Datum		result;
 
 	/* Check whether the function is provided. */
-	funcid = get_opfamily_proc(opfamily, ANYOID, ANYOID, GIST_STRATNUM_PROC);
+	funcid = get_opfamily_proc(opfamily, ANYOID, ANYOID, GIST_TRANSLATE_CMPTYPE_PROC);
 	if (!OidIsValid(funcid))
 		return InvalidStrategy;
 
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 2a49e6d20f0..2ed6f74fce9 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -138,7 +138,7 @@ gistvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, VOIDOID, true,
 											1, 1, INTERNALOID);
 				break;
-			case GIST_STRATNUM_PROC:
+			case GIST_TRANSLATE_CMPTYPE_PROC:
 				ok = check_amproc_signature(procform->amproc, INT2OID, true,
 											1, 1, INT4OID) &&
 					procform->amproclefttype == ANYOID &&
@@ -265,7 +265,7 @@ gistvalidate(Oid opclassoid)
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
 			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
-			i == GIST_STRATNUM_PROC)
+			i == GIST_TRANSLATE_CMPTYPE_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -336,7 +336,7 @@ gistadjustmembers(Oid opfamilyoid,
 			case GIST_FETCH_PROC:
 			case GIST_OPTIONS_PROC:
 			case GIST_SORTSUPPORT_PROC:
-			case GIST_STRATNUM_PROC:
+			case GIST_TRANSLATE_CMPTYPE_PROC:
 				/* Optional, so force it to be a soft family dependency */
 				op->ref_is_hard = false;
 				op->ref_is_family = true;
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index db78e60eeab..b3f4e02cbfd 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -40,7 +40,7 @@
 #define GIST_FETCH_PROC					9
 #define GIST_OPTIONS_PROC				10
 #define GIST_SORTSUPPORT_PROC			11
-#define GIST_STRATNUM_PROC				12
+#define GIST_TRANSLATE_CMPTYPE_PROC		12
 #define GISTNProcs					12
 
 /*
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 92505148998..e3477500baa 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -533,7 +533,7 @@
   amprocrighttype => 'box', amprocnum => '8', amproc => 'gist_box_distance' },
 { amprocfamily => 'gist/box_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '1',
   amproc => 'gist_poly_consistent' },
@@ -555,7 +555,7 @@
   amproc => 'gist_poly_distance' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 { amprocfamily => 'gist/circle_ops', amproclefttype => 'circle',
   amprocrighttype => 'circle', amprocnum => '1',
   amproc => 'gist_circle_consistent' },
@@ -576,7 +576,7 @@
   amproc => 'gist_circle_distance' },
 { amprocfamily => 'gist/circle_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector',
   amprocrighttype => 'tsvector', amprocnum => '1',
   amproc => 'gtsvector_consistent(internal,tsvector,int2,oid,internal)' },
@@ -636,7 +636,7 @@
   amproc => 'range_sortsupport' },
 { amprocfamily => 'gist/range_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -655,7 +655,7 @@
   amprocrighttype => 'inet', amprocnum => '9', amproc => 'inet_gist_fetch' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '1',
   amproc => 'multirange_gist_consistent' },
@@ -676,7 +676,7 @@
   amproc => 'range_gist_same' },
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'any',
   amprocrighttype => 'any', amprocnum => '12',
-  amproc => 'gist_stratnum_common' },
+  amproc => 'gist_translate_cmptype_common' },
 
 # gin
index 37a484147a8..d3d28a263fa 100644
@@ -12543,9 +12543,9 @@
 
 # GiST stratnum implementations
 { oid => '8047', descr => 'GiST support',
-  proname => 'gist_stratnum_common', prorettype => 'int2',
+  proname => 'gist_translate_cmptype_common', prorettype => 'int2',
   proargtypes => 'int4',
-  prosrc => 'gist_stratnum_common' },
+  prosrc => 'gist_translate_cmptype_common' },
 
 # AIO related functions
 { oid => '9200', descr => 'information about in-progress asynchronous IOs',
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index cc517ed5e90..c3b2b9d8603 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -890,17 +890,17 @@ SELECT pg_column_toast_chunk_id(a) IS NULL,
 
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
--- test stratnum support functions
-SELECT gist_stratnum_common(7);
- gist_stratnum_common 
-----------------------
-                    3
+-- test stratnum translation support functions
+SELECT gist_translate_cmptype_common(7);
+ gist_translate_cmptype_common 
+-------------------------------
+                             3
 (1 row)
 
-SELECT gist_stratnum_common(3);
- gist_stratnum_common 
-----------------------
-                   18
+SELECT gist_translate_cmptype_common(3);
+ gist_translate_cmptype_common 
+-------------------------------
+                            18
 (1 row)
 
 -- relpath tests
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 5f9c77512d1..23792c4132a 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -400,9 +400,9 @@ CREATE TABLE test_chunk_id (a TEXT, b TEXT STORAGE EXTERNAL);
 DROP TABLE test_chunk_id;
 DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool);
 
--- test stratnum support functions
-SELECT gist_stratnum_common(7);
-SELECT gist_stratnum_common(3);
+-- test stratnum translation support functions
+SELECT gist_translate_cmptype_common(7);
+SELECT gist_translate_cmptype_common(3);

 -- relpath tests
-- 
2.49.0

---

From: Paul A Jungwirth <[email-masked]>
Date: Mon, 26 May 2025 14:18:54 -0700

> thing on different levels.  What do you think?  (This would be for PG18.)

I agree this rename makes sense.

Here do we want to say "respective operator class" instead of
"respective operator family"? Or "operator class/family"? Technically
btree_gist attaches it to the whole opfamily, but that's only because
there is no appropriate ALTER OPERATOR CLASS functionality:

+      <para>

Yours,

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 2 Jun 2025 09:31:24 +0200

> there is no appropriate ALTER OPERATOR CLASS functionality:

Thanks, I have committed it as is.  The function is part of the operator 
family; I guess there could be different interpretations about why that 
is so, but I think this would introduce more confusion if we somehow 
talked about operator classes in this context.

---

From: Tom Lane <[email-masked]>
Date: Mon, 02 Jun 2025 09:50:45 -0400

> talked about operator classes in this context.

GIST and GIN have traditionally not made any distinction between
operator classes and families: they're always one-class-per-family.
I guess that's because they cater more to one-off opclasses where
there is not meaningful commonality of semantics across opclasses,
nor the possibility of operators belonging to multiple opclasses.

That being the case, I'm hesitant to spend a lot of time worrying
about whether particular behavior belongs at the class or family
level.  Without concrete examples to look at, there's little hope
of getting it right anyway.  So I'm content with Peter's choice
here.  Perhaps sometime in the future we will have useful examples
with which to revisit this question.

[ wanders away wondering about recasting btree_gist and btree_gin
as single opfamilies ... ]

			regards, tom lane