Subject: Re: SQL:2011 application time

From: Vik Fearing <[email-masked]>
Date: Fri, 1 Sep 2023 12:50:08 +0200

On 9/1/23 11:30, Peter Eisentraut wrote:
> 1) If I write UNIQUE (a, b, c WITHOUT OVERLAPS), does the WITHOUT 
> OVERLAPS clause attach to the last column, or to the whole column list? 
> In the SQL standard, you can only have one period and it has to be 
> listed last, so this question does not arise.  But here we are building 
> a more general facility to then build the SQL facility on top of.  So I 
> think it doesn't make sense that the range column must be last or that 
> there can only be one.  Also, your implementation requires at least one 
> non-overlaps column, which also seems like a confusing restriction.
> 
> I think the WITHOUT OVERLAPS clause should be per-column, so that 
> something like UNIQUE (a WITHOUT OVERLAPS, b, c WITHOUT OVERLAPS) would 
> be possible.  Then the WITHOUT OVERLAPS clause would directly correspond 
> to the choice between equality or overlaps operator per column.
> 
> An alternative interpretation would be that WITHOUT OVERLAPS applies to 
> the whole column list, and we would take it to mean, for any range 
> column, use the overlaps operator, for any non-range column, use the 
> equals operator.  But I think this would be confusing and would prevent 
> the case of using the equality operator for some ranges and the overlaps 
> operator for some other ranges in the same key.

I prefer the first option.  That is: WITHOUT OVERLAPS applies only to 
the column or expression it is attached to, and need not be last in line.
-- 
Vik Fearing

---

From: Paul Jungwirth <[email-masked]>
Date: Fri, 1 Sep 2023 12:56:40 -0700

On 9/1/23 03:50, Vik Fearing wrote:
>> OVERLAPS clause attach to the last column, or to the whole column 
>> list? In the SQL standard, you can only have one period and it has to 
>> be listed last, so this question does not arise.  But here we are 
>> building a more general facility to then build the SQL facility on top 
>> of.  So I think it doesn't make sense that the range column must be 
>> last or that there can only be one.  Also, your implementation 
>> requires at least one non-overlaps column, which also seems like a 
>> confusing restriction.
>>
>> something like UNIQUE (a WITHOUT OVERLAPS, b, c WITHOUT OVERLAPS) 
>> would be possible.  Then the WITHOUT OVERLAPS clause would directly 
>> correspond to the choice between equality or overlaps operator per 
>> column.
>>
>> An alternative interpretation would be that WITHOUT OVERLAPS applies 
>> to the whole column list, and we would take it to mean, for any range 
>> equals operator.  But I think this would be confusing and would 
>> prevent the case of using the equality operator for some ranges and 
>> the overlaps operator for some other ranges in the same key.
> 

I agree. The second option seems confusing and is more restrictive.

I think allowing multiple uses of `WITHOUT OVERLAPS` (and in any 
position) is a great recommendation that enables a lot of new 
functionality. Several books[1,2] about temporal databases describe a 
multi-dimensional temporal space (even beyond application time vs. 
system time), and the standard is pretty disappointing here. It's not a 
weird idea.

But I just want to be explicit that this isn't something the standard 
describes. (I think everyone in the conversation so far understands 
that.) So far I've tried to be pretty scrupulous about following 
SQL:2011, although personally I'd rather see Postgres support this 
functionality. And it's not like it goes *against* what the standard 
says. But if there are any objections, I'd love to hear them before 
putting in the work. :-)

If we allow multiple+anywhere WITHOUT OVERLAPS in PRIMARY KEY & UNIQUE 
constraints, then surely we also allow multiple+anywhere PERIOD in 
FOREIGN KEY constraints too. (I guess the standard switched keywords 
because a FK is more like "MUST OVERLAPS". :-)

Also if you have multiple application-time dimensions we probably need 
to allow multiple FOR PORTION OF clauses. I think the syntax would be:

UPDATE t
   FOR PORTION OF valid_at FROM ... TO ...
   FOR PORTION OF asserted_at FROM ... TO ...
   [...]
   SET foo = bar

Does that sound okay?

I don't quite understand this part:

 >> Also, your implementation
 >> requires at least one non-overlaps column, which also seems like a
 >> confusing restriction.

That's just a regular non-temporal constraint. Right? If I'm missing 
something let me know.

[1] C. J. Date, Hugh Darwen, Nikos Lorentzos. Time and Relational 
Theory, Second Edition: Temporal Databases in the Relational Model and 
SQL. 2nd edition, 2014.
[2] Tom Johnston. Bitemporal Data: Theory and Practice. 2014.

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

---

From: Vik Fearing <[email-masked]>
Date: Sat, 2 Sep 2023 00:41:13 +0200

On 9/1/23 21:56, Paul Jungwirth wrote:
>>> building a more general facility to then build the SQL facility on 
>>> top of.  So I think it doesn't make sense that the range column must 
>>> be last or that there can only be one.  Also, your implementation 

I have no problem with a first version doing exactly what the standard 
says and expanding it later.

> because a FK is more like "MUST OVERLAPS". :-)

Seems reasonable.

>    FOR PORTION OF asserted_at FROM ... TO ...

That sounds really cool.

> [2] Tom Johnston. Bitemporal Data: Theory and Practice. 2014.

Thanks!  I have ordered these books.
-- 
Vik Fearing

---

From: jian he <[email-masked]>
Date: Fri, 8 Sep 2023 09:24:22 +0800

> That's just a regular non-temporal constraint. Right? If I'm missing

for a range primary key, is it fine to expect it to be unique, not
null and also not overlap? (i am not sure how hard to implement it).

-----------------------------------------------------------------
quote from 7IWD2-02-Foundation-2011-12.pdf. 4.18.3.2 Unique
constraints, page 97 of 1483.

4.18.3.2 Unique constraints In addition to the components of every
table constraint descriptor, a unique constraint descriptor includes:
— An indication of whether it was defined with PRIMARY KEY or UNIQUE.
— The names and positions of the unique columns specified in the
<unique column list>
 — If <without overlap specification> is specified, then the name of
the period specified.

If the table descriptor for base table T includes a unique constraint
descriptor indicating that the unique constraint was defined with
PRIMARY KEY, then the columns of that unique constraint constitute the
primary key of T. A table that has a primary key cannot have a proper
supertable.
A unique constraint that does not include a <without overlap
specification> on a table T is satisfied if and only if there do not
exist two rows R1 and R2 of T such that R1 and R2 have the same
non-null values in the unique columns. If a unique constraint UC on a
table T includes a <without overlap specification> WOS, then let
<application time period name> ATPN be the contained in WOS. UC is
satisfied if and only if there do not exist two rows R1 and R2 of T
such that R1 and R2 have the same non-null values in the unique
columns and the ATPN period values of R1 and R2 overlap. In addition,
if the unique constraint was defined with PRIMARY KEY, then it
requires that none of the values in the specified column or columns be
a null value.
-----------------------------------------------------------------
based on the above, the unique constraint does not specify that the
column list must be range type. UNIQUE (a, c WITHOUT OVERLAPS).
Here column "a" can be a range type (that have overlap property) and
can be not.
In fact, many of your primary key, foreign key regess test using
something like '[11,11]' (which make it more easy to understand),
which in logic is a non-range usage.
So UNIQUE (a, c WITHOUT OVERLAPS), column "a" be a non-range data type
does make sense?

---

From: jian he <[email-masked]>
Date: Fri, 8 Sep 2023 17:35:10 +0800

hi.
the following script makes the server crash (Segmentation fault).

create schema test;
set search_path to test;
DROP TABLE  IF EXISTS 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);

INSERT INTO temporal_rng VALUES
    ('[11,11]', daterange('2018-01-01', '2020-01-01')),
    ('[11,11]', daterange('2020-01-01', '2021-01-01')),
    ('[20,20]', daterange('2018-01-01', '2020-01-01')),
    ('[20,20]', daterange('2020-01-01', '2021-01-01'));

DROP TABLE  IF EXISTS temporal_fk_rng2rng;

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)
);
---------------------------------------------------------------
BEGIN;
ALTER TABLE temporal_fk_rng2rng DROP CONSTRAINT IF EXISTS
temporal_fk_rng2rng_fk;
ALTER TABLE temporal_fk_rng2rng
  ALTER COLUMN parent_id SET DEFAULT '[-1,-1]',
  ALTER COLUMN valid_at SET DEFAULT tsrange('2018-01-01', '2019-11-11');

ALTER TABLE temporal_fk_rng2rng
ADD CONSTRAINT temporal_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng
        on update set DEFAULT
        on delete set DEFAULT;
---------------------------------------------------------gdb related
info:---------------
(gdb) continue
Continuing.

Program received signal SIGSEGV, Segmentation fault.
FindFKComparisonOperators (fkconstraint=0x556450100bd8,
tab=0x55644ff8f570, i=1, fkattnum=0x7ffeb3286ba0,
old_check_ok=0x7ffeb3286b11, old_pfeqop_item=0x7ffeb3286b28,
pktype=3912, fktype=3908, opclass=10078, is_temporal=true,
for_overlaps=true, pfeqopOut=0x7ffeb3286da4, ppeqopOut=0x7ffeb3286e24,
ffeqopOut=0x7ffeb3286ea4) at
../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:11582
11582                           pkattr_name = strVal(fkconstraint->pk_period);
(gdb) where
#0  FindFKComparisonOperators (fkconstraint=0x556450100bd8,
tab=0x55644ff8f570, i=1,
    fkattnum=0x7ffeb3286ba0, old_check_ok=0x7ffeb3286b11,
old_pfeqop_item=0x7ffeb3286b28, pktype=3912,
    fktype=3908, opclass=10078, is_temporal=true, for_overlaps=true,
pfeqopOut=0x7ffeb3286da4,
    ppeqopOut=0x7ffeb3286e24, ffeqopOut=0x7ffeb3286ea4)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:11582
#1  0x000055644e53875a in ATAddForeignKeyConstraint
(wqueue=0x7ffeb3287118, tab=0x55644ff8f570,
    rel=0x7fb2dc124430, fkconstraint=0x556450100bd8, recurse=true,
recursing=false, lockmode=6)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:10395
#2  0x000055644e536cc2 in ATExecAddConstraint (wqueue=0x7ffeb3287118,
tab=0x55644ff8f570,
    rel=0x7fb2dc124430, newConstraint=0x556450100bd8, recurse=true,
is_readd=false, lockmode=6)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:9948
#3  0x000055644e528eaa in ATExecCmd (wqueue=0x7ffeb3287118,
tab=0x55644ff8f570, cmd=0x5564500fae48,
    lockmode=6, cur_pass=10, context=0x7ffeb3287310)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:5711
#4  0x000055644e5283f6 in ATRewriteCatalogs (wqueue=0x7ffeb3287118,
lockmode=6, context=0x7ffeb3287310)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:5569
#5  0x000055644e527031 in ATController (parsetree=0x55645000e228,
rel=0x7fb2dc124430,
    cmds=0x55645000e1d8, recurse=true, lockmode=6, context=0x7ffeb3287310)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:5136
#6  0x000055644e526a9d in AlterTable (stmt=0x55645000e228, lockmode=6,
context=0x7ffeb3287310)
    at ../../Desktop/pg_sources/main/postgres/src/backend/commands/tablecmds.c:4789
#7  0x000055644e92eb65 in ProcessUtilitySlow (pstate=0x55644ff8f460,
pstmt=0x55645000e2d8,
--Type <RET> for more, q to quit, c to continue without paging--
    55645000d330 "ALTER TABLE temporal_fk_rng2rng\n\tADD CONSTRAINT
temporal_fk_rng2rng_fk\n\tFOREIGN KEY (parent_id, PERIOD
valid_at)\n\tREFERENCES temporal_rng\n        on update set DEFAULT \n
       on delete set DEFAULT;", context=PROCESS_UTILITY_TOPLEVEL,
params=0x0, queryEnv=0x0,
    dest=0x55645000e698, qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/utility.c:1329
#8  0x000055644e92e24c in standard_ProcessUtility (pstmt=0x55645000e2d8,
    queryString=0x55645000d330 "ALTER TABLE temporal_fk_rng2rng\n\tADD
CONSTRAINT temporal_fk_rng2rng_fk\n\tFOREIGN KEY (parent_id, PERIOD
valid_at)\n\tREFERENCES temporal_rng\n        on update set DEFAULT \n
       on delete set DEFAULT;", readOnlyTree=false,
context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
    queryEnv=0x0, dest=0x55645000e698, qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/utility.c:1078
#9  0x000055644e92c921 in ProcessUtility (pstmt=0x55645000e2d8,
    queryString=0x55645000d330 "ALTER TABLE temporal_fk_rng2rng\n\tADD
CONSTRAINT temporal_fk_rng2rng_fk\n\tFOREIGN KEY (parent_id, PERIOD
valid_at)\n\tREFERENCES temporal_rng\n        on update set DEFAULT \n
       on delete set DEFAULT;", readOnlyTree=false,
context=PROCESS_UTILITY_TOPLEVEL, params=0x0,
    queryEnv=0x0, dest=0x55645000e698, qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/utility.c:530
#10 0x000055644e92a83e in PortalRunUtility (portal=0x5564500a9840,
pstmt=0x55645000e2d8,
    isTopLevel=true, setHoldSnapshot=false, dest=0x55645000e698,
qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/pquery.c:1158
#11 0x000055644e92abdb in PortalRunMulti (portal=0x5564500a9840,
isTopLevel=true,
    setHoldSnapshot=false, dest=0x55645000e698,
altdest=0x55645000e698, qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/pquery.c:1315
#12 0x000055644e929b53 in PortalRun (portal=0x5564500a9840,
count=9223372036854775807, isTopLevel=true,
    run_once=true, dest=0x55645000e698, altdest=0x55645000e698,
qc=0x7ffeb3287970)
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/pquery.c:791
#13 0x000055644e91f206 in exec_simple_query (
    query_string=0x55645000d330 "ALTER TABLE
temporal_fk_rng2rng\n\tADD CONSTRAINT
temporal_fk_rng2rng_fk\n\tFOREIGN KEY (parent_id, PERIOD
valid_at)\n\tREFERENCES temporal_rng\n        on update set DEFAULT \n
       on delete set DEFAULT;")
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/postgres.c:1274
--Type <RET> for more, q to quit, c to continue without paging--
#14 0x000055644e926c49 in PostgresMain (dbname=0x556450045610 "regression",
    username=0x5564500455f8 "jian")
    at ../../Desktop/pg_sources/main/postgres/src/backend/tcop/postgres.c:4637
#15 0x000055644e7ff0e9 in BackendRun (port=0x5564500394f0)
    at ../../Desktop/pg_sources/main/postgres/src/backend/postmaster/postmaster.c:4438
#16 0x000055644e7fe6a1 in BackendStartup (port=0x5564500394f0)
    at ../../Desktop/pg_sources/main/postgres/src/backend/postmaster/postmaster.c:4166
#17 0x000055644e7f8aa0 in ServerLoop ()
    at ../../Desktop/pg_sources/main/postgres/src/backend/postmaster/postmaster.c:1780
#18 0x000055644e7f8042 in PostmasterMain (argc=3, argv=0x55644ff77e60)
    at ../../Desktop/pg_sources/main/postgres/src/backend/postmaster/postmaster.c:1464
#19 0x000055644e67f884 in main (argc=3, argv=0x55644ff77e60)
    at ../../Desktop/pg_sources/main/postgres/src/backend/main/main.c:198

---

From: Paul A Jungwirth <[email-masked]>
Date: Fri, 8 Sep 2023 10:26:59 -0700

On Fri, Sep 8, 2023 at 2:35 AM jian he <[email-masked]> wrote:
> [snip]

Thank you for the report! It looks like I forgot to handle implicit
column names after REFERENCES. The PERIOD part needs to get looked up
from the PK as we do for normal FK attrs. I'll add that to the next
patch.

Yours,
Paul

---

From: jian he <[email-masked]>
Date: Sat, 9 Sep 2023 15:54:37 +0800

hi
I am confused by (pk,fk) on delete on update (restriction and no
action) result based on v13.
related post: https://stackoverflow.com/questions/14921668/difference-between-restrict-and-no-action
Please check the following test and comments.

---common setup for test0, test1,test2,test3
BEGIN;
DROP TABLE IF EXISTS temporal_rng, temporal_fk_rng2rng;
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);
CREATE TABLE temporal_fk_rng2rng (
id int4range,
valid_at tsrange,
parent_id int4range
);
commit;

----------------no_action_vs_restriction test0
BEGIN;
ALTER TABLE temporal_fk_rng2rng
DROP CONSTRAINT IF EXISTS 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 NO ACTION
ON UPDATE NO ACTION;
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]');

/*
expect below to fail.
since to be deleted range is being referenced (in temporal_fk_rng2rng)
but the v13 patch won't fail.
*/
delete from temporal_rng
    FOR PORTION OF valid_at FROM '2018-01-06' TO '2018-01-11'
WHERE   id = '[5,5]'
AND     valid_at @> '2018-01-05'::timestamp;
TABLE temporal_rng \; table temporal_fk_rng2rng;
ROLLBACK;

----------------no_action_vs_restriction test1
BEGIN;
ALTER TABLE temporal_fk_rng2rng
DROP CONSTRAINT IF EXISTS 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
ON UPDATE RESTRICT;
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]');

/*
expect the below command not to fail.
since to be deleted range is not being referenced in temporal_fk_rng2rng)
but the v13 patch will fail.
*/
delete from temporal_rng
    FOR PORTION OF valid_at FROM '2018-01-12' TO '2018-01-20'
WHERE   id = '[5,5]'
AND     valid_at @> '2018-01-05'::timestamp;
ROLLBACK;

----------------no_action_vs_restriction test2
BEGIN;
ALTER TABLE temporal_fk_rng2rng
DROP CONSTRAINT IF EXISTS 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 no action
ON UPDATE no action;
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]');
/*
expect below command fail.
since to be deleted range is being referenced (in temporal_fk_rng2rng)
*/
UPDATE temporal_rng FOR PORTION OF valid_at FROM '2018-01-06' TO '2018-01-08'
SET     id = '[7,7]'
WHERE   id = '[5,5]'
AND     valid_at @> '2018-01-05'::timestamp;
TABLE temporal_rng \; table temporal_fk_rng2rng;

ROLLBACK;

----------------no_action_vs_restriction test3
BEGIN;
ALTER TABLE temporal_fk_rng2rng
DROP CONSTRAINT IF EXISTS 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
ON UPDATE RESTRICT;
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]');

/*
expect the below command not to fail.
since to be deleted range is not being referenced in temporal_fk_rng2rng)
but the v13 patch will fail.
*/
UPDATE temporal_rng FOR PORTION OF valid_at FROM '2018-01-12' TO '2018-01-20'
SET     id = '[7,7]'
WHERE   id = '[5,5]'
AND     valid_at @> '2018-01-05'::timestamp;
ROLLBACK;

---

From: jian he <[email-masked]>
Date: Tue, 12 Sep 2023 17:01:36 +0800

hi. some trivial issue:

in src/backend/catalog/index.c
/* * System attributes are never null, so no need to check. */
if (attnum <= 0)

since you already checked attnum == 0
so here you can just attnum < 0?
-------------------------------------------------
ERROR:  column "valid_at" named in WITHOUT OVERLAPS is not a range type

IMHO, "named" is unnecessary.
-------------------------------------------------
doc/src/sgml/catalogs.sgml
pg_constraint adds another attribute (column): contemporal, seems no doc entry.

also the temporal in oxford definition is "relating to time", here we
can deal with range.
So maybe  "temporal" is not that accurate?

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 14 Sep 2023 09:09:19 -0700

On 9/7/23 18:24, jian he wrote:
> ...

I'm not sure I understand this question, but here are a few things that 
might help clarify things:

In SQL:2011, a temporal primary key, unique constraint, or foreign key 
may have one or more "scalar" parts (just like a regular key) followed 
by one "PERIOD" part, which is denoted with "WITHOUT OVERLAPS" (in 
PKs/UNIQUEs) or "PERIOD" (in FKs). Except for this last key part, 
everything is still compared for equality, just as in a traditional key. 
But this last part is compared for overlaps. It's exactly the same as 
`EXCLUDE (id WITH =, valid_at WITH &&)`. The overlap part must come last 
and you can have only one (but you may have more than one scalar part if 
you like).

In the patch, I have followed that pattern, except I also allow a 
regular range column anywhere I allow a PERIOD. In fact PERIODs are 
mostly implemented on top of range types. (Until recently PERIOD support 
was in the first patch, not the last, and there was code all throughout 
for handling both, e.g. within indexes, etc. But at pgcon Peter 
suggested building everything on just range columns, and then having 
PERIODs create an "internal" GENERATED column, and that cleaned up the 
code considerably.)

One possible source of confusion is that in the tests I'm using range 
columns *also* for the scalar key part. So valid_at is a tsrange, and 
int is an int4range. This is not normally how you'd use the feature, but 
you need the btree_gist extension to mix int & tsrange (e.g.), and 
that's not available in the regress tests. We are still comparing the 
int4range for regular equality and the tsrange for overlaps. If you 
search this thread there was some discussion about bringing btree_gist 
into core, but it sounds like it doesn't need to happen. (It might be 
still desirable independently. EXCLUDE constraints are also not really 
something you can use practically without it, and their tests use the 
same trick of comparing ranges for plain equality.)

The piece of discussion you're replying to is about allowing *multiple* 
WITHOUT OVERLAPS modifiers on a PK/UNIQUE constraint, and in any 
position. I think that's a good idea, so I've started adapting the code 
to support it. (In fact there is a lot of code that assumes the overlaps 
key part will be in the last position, and I've never really been happy 
with that, so it's an excuse to make that more robust.) Here I'm saying 
(1) you will still need at least one scalar key part, (2) if there are 
no WITHOUT OVERLAPS parts then you just have a regular key, not a 
temporal one, (3) changing this obliges us to do the same for foreign 
keys and FOR PORTION OF.

I hope that helps! I apologize if I've completely missed the point. If 
so please try again. :-)

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 14 Sep 2023 09:11:02 -0700

Thanks for the thorough review and testing!

Here is a v14 patch with the segfault and incorrect handling of NO 
ACTION and RESTRICT fixed (and reproductions added to the test suite).

A few more comments below on feedback from you and Peter:

> so here you can just attnum < 0?

I fixed the "/* *" typo here. I'm reluctant to change the attnum 
comparison since that's not a line I touched. (It was just part of the 
context around the updated comment.) Your suggestion does make sense 
though, so perhaps it should be a separate commit?

> IMHO, "named" is unnecessary.

Changed.

> pg_constraint adds another attribute (column): contemporal, seems no doc entry.

Added.

> So maybe  "temporal" is not that accurate?

I agree if we allow multiple WITHOUT OVERLAPS/etc clauses, we should 
change the terminology. I'll include that with the multiple-range-keys 
change discussed upthread.

On 9/1/23 02:30, Peter Eisentraut wrote:
 > * There is a lot of talk about "temporal" in this patch, but this
 > functionality is more general than temporal.  I would prefer to change
 > this to more neutral terms like "overlaps".

Okay, sounds like several of us agree on this.

 > * The field ii_Temporal in IndexInfo doesn't seem necessary and could
 > be  handled via local variables.  See [0] for a similar discussion:
 >
 > [0]:
 > 
https://www.postgresql.org/message-id/flat/[email-masked]

Done.

 > * In gram.y, change withoutOverlapsClause -> without_overlaps_clause
 > for consistency with the surrounding code.

Done.

 > * No-op assignments like n->without_overlaps = NULL; can be omitted.
 > (Or you should put them everywhere.  But only in some places seems
 > inconsistent and confusing.)

Changed. That makes sense since newNode uses palloc0fast. FWIW there is 
quite a lot of other code in gram.y that sets NULL fields though, 
including in ConstraintElem, and it seems like it does improve the 
clarity a little. By "everywhere" I think you mean wherever the file 
calls makeNode(Constraint)? I might go back and do it that way later.

I'll keep working on a patch to support multiple range keys, but I 
wanted to work through the rest of the feedback first. Also there is 
some fixing to do with partitions I believe, and then I'll finish the 
PERIOD support. So this v14 patch is just some minor fixes & tweaks from 
September feedback.

Yours,

From 53169b4909c1e06cf83a62cd7b35bf22f9d5b33e Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v14 1/5] 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.
---
 doc/src/sgml/catalogs.sgml                    |  12 +
 doc/src/sgml/ref/create_table.sgml            |  38 +-
 src/backend/catalog/heap.c                    |   2 +
 src/backend/catalog/index.c                   |   8 +-
 src/backend/catalog/pg_constraint.c           |   2 +
 src/backend/commands/indexcmds.c              | 155 +++++++-
 src/backend/commands/tablecmds.c              |  14 +-
 src/backend/commands/trigger.c                |   1 +
 src/backend/commands/typecmds.c               |   1 +
 src/backend/parser/gram.y                     |  30 +-
 src/backend/parser/parse_utilcmd.c            | 124 +++++-
 src/backend/utils/adt/ruleutils.c             |  29 +-
 src/backend/utils/cache/relcache.c            |  10 +-
 src/bin/pg_dump/pg_dump.c                     |  35 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  46 +++
 src/bin/psql/describe.c                       |  12 +-
 src/include/catalog/index.h                   |   1 +
 src/include/catalog/pg_constraint.h           |   9 +-
 src/include/commands/defrem.h                 |   3 +-
 src/include/nodes/execnodes.h                 |   1 +
 src/include/nodes/parsenodes.h                |   4 +
 .../regress/expected/without_overlaps.out     | 359 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/without_overlaps.sql     | 262 +++++++++++++
 25 files changed, 1101 insertions(+), 60 deletions(-)
 create mode 100644 src/test/regress/expected/without_overlaps.out
 create mode 100644 src/test/regress/sql/without_overlaps.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d17ff51e28..19c2e1c827 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2709,6 +2709,18 @@ 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>contemporal</structfield> <type>bool</type>
+      </para>
+      <para>
+       This constraint is a application-time temporal constraint,
+       defined with <literal>WITHOUT OVERLAPS</literal> (for primary
+       key and unique constraints) or <literal>PERIOD</literal> (for
+       foreign keys).
+      </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/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e04a0692c4..87f0aab13b 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">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</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
@@ -107,6 +107,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
 { <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]
 
+<phrase><replaceable class="parameter">temporal_interval</replaceable> in a <literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>, or <literal>FOREIGN KEY</literal> constraint is:</phrase>
+
+<replaceable class="parameter">range_column_name</replaceable>
+
 <phrase><replaceable class="parameter">referential_action</replaceable> in a <literal>FOREIGN KEY</literal>/<literal>REFERENCES</literal> constraint is:</phrase>
 
 { NO ACTION | RESTRICT | CASCADE | SET NULL [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | SET DEFAULT [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] }
@@ -1001,7 +1005,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.
+      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 and behave like a temporal <literal>PRIMARY
+      KEY</literal>: preventing duplicates only in overlapping time periods.
      </para>
 
      <para>
@@ -1019,7 +1026,8 @@ 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">temporal_interval</replaceable> WITHOUT OVERLAPS ] )</literal>
     <optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
     <listitem>
      <para>
@@ -1053,8 +1061,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.
+      create a unique btree index (or GiST if temporal) on the column or group of
+      columns used in the constraint.
      </para>
 
      <para>
@@ -1067,6 +1075,24 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       (e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
       index deletion.
      </para>
+
+     <para>
+      A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
+      is a <emphasis>temporal</emphasis> primary key.
+      The <literal>WITHOUT OVERLAPS</literal> value
+      must be a range type and is used to constrain the record's applicability
+      to just that interval (usually a range of dates or timestamps).
+      The main part of the primary key may be repeated in other rows of the table,
+      as long as records with the same key don't overlap in the
+      <literal>WITHOUT OVERLAPS</literal> column.
+     </para>
+
+     <para>
+      A temporal <literal>PRIMARY KEY</literal> is enforced with an
+      <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
+      constraint, backed by a GiST index. You may need to install the
+      <xref linkend="btree-gist"/> extension to create temporal primary keys.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b42711f574..7148dd1787 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2140,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 							  is_local, /* conislocal */
 							  inhcount, /* coninhcount */
 							  is_no_inherit,	/* connoinherit */
+							  false,	/* contemporal */
 							  is_internal); /* internally constructed? */
 
 	pfree(ccbin);
@@ -2190,6 +2191,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
+							  false,	/* contemporal */
 							  false);
 	return constrOid;
 }
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 72f476b51d..1a617b042d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -255,8 +255,8 @@ index_check_primary_key(Relation heapRel,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("primary keys cannot be expressions")));
 
-		/* System attributes are never null, so no need to check */
-		if (attnum < 0)
+		/* System attributes are never null, so no need to check. */
+		if (attnum <= 0)
 			continue;
 
 		atttuple = SearchSysCache2(ATTNUM,
@@ -1897,6 +1897,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_TEMPORAL: constraint is for a temporal primary key
  * allow_system_table_mods: allow table to be a system catalog
  * is_internal: index is constructed due to internal process
  */
@@ -1920,11 +1921,13 @@ index_constraint_create(Relation heapRelation,
 	bool		mark_as_primary;
 	bool		islocal;
 	bool		noinherit;
+	bool		is_temporal;
 	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_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
 
 	/* constraint creation support doesn't work while bootstrapping */
 	Assert(!IsBootstrapProcessingMode());
@@ -2001,6 +2004,7 @@ index_constraint_create(Relation heapRelation,
 								   islocal,
 								   inhcount,
 								   noinherit,
+								   is_temporal,	/* contemporal */
 								   is_internal);
 
 	/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index e9d4d6006e..d5329ca8c9 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 conTemporal,
 					  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_contemporal - 1] = BoolGetDatum(conTemporal);
 
 	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 ab8b81b302..794bde190e 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -73,6 +73,11 @@
 /* non-export function prototypes */
 static bool CompareOpclassOptions(const Datum *opts1, const Datum *opts2, int natts);
 static void CheckPredicate(Expr *predicate);
+static void get_index_attr_temporal_operator(Oid opclass,
+											 Oid atttype,
+											 bool isoverlaps,
+											 Oid *opid,
+											 int *strat);
 static void ComputeIndexAttrs(IndexInfo *indexInfo,
 							  Oid *typeOids,
 							  Oid *collationOids,
@@ -85,6 +90,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
 							  Oid accessMethodId,
 							  bool amcanorder,
 							  bool isconstraint,
+							  bool istemporal,
 							  Oid ddl_userid,
 							  int ddl_sec_context,
 							  int *ddl_save_nestlevel);
@@ -142,6 +148,7 @@ typedef struct ReindexErrorInfo
  *		to index on.
  * 'exclusionOpNames': list of names of exclusion-constraint operators,
  *		or NIL if not an exclusion constraint.
+ * 'istemporal': 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
@@ -171,7 +178,8 @@ bool
 CheckIndexCompatible(Oid oldId,
 					 const char *accessMethodName,
 					 const List *attributeList,
-					 const List *exclusionOpNames)
+					 const List *exclusionOpNames,
+					 bool istemporal)
 {
 	bool		isconstraint;
 	Oid		   *typeIds;
@@ -244,8 +252,8 @@ CheckIndexCompatible(Oid oldId,
 					  coloptions, attributeList,
 					  exclusionOpNames, relationId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, isconstraint, InvalidOid, 0, NULL);
-
+					  amcanorder, isconstraint, istemporal, InvalidOid,
+					  0, NULL);
 
 	/* Get the soon-obsolete pg_index tuple. */
 	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -553,6 +561,7 @@ DefineIndex(Oid tableId,
 	bool		amcanorder;
 	bool		amissummarizing;
 	amoptions_function amoptions;
+	bool		exclusion;
 	bool		partitioned;
 	bool		safe_index;
 	Datum		reloptions;
@@ -671,6 +680,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->istemporal;
+
 	/* Ensure that it makes sense to index this kind of relation */
 	switch (rel->rd_rel->relkind)
 	{
@@ -839,7 +854,7 @@ DefineIndex(Oid tableId,
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
 								 accessMethodId);
 
-	if (stmt->unique && !amRoutine->amcanunique)
+	if (stmt->unique && !stmt->istemporal && !amRoutine->amcanunique)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("access method \"%s\" does not support unique indexes",
@@ -854,7 +869,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",
@@ -906,8 +921,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->istemporal,
+					  root_save_userid, root_save_sec_context,
+					  &root_save_nestlevel);
 
 	/*
 	 * Extra checks when creating a PRIMARY KEY index.
@@ -925,7 +941,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;
@@ -933,6 +949,8 @@ DefineIndex(Oid tableId,
 
 		if (stmt->primary)
 			constraint_type = "PRIMARY KEY";
+		else if (stmt->unique && stmt->istemporal)
+			constraint_type = "temporal UNIQUE";
 		else if (stmt->unique)
 			constraint_type = "UNIQUE";
 		else if (stmt->excludeOpNames)
@@ -979,10 +997,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->istemporal && accessMethodId != BTREE_AM_OID)
 				ereport(ERROR,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("cannot match partition key to an index using access method \"%s\"",
@@ -1015,12 +1033,12 @@ DefineIndex(Oid tableId,
 					{
 						Oid			idx_eqop = InvalidOid;
 
-						if (stmt->unique)
+						if (stmt->unique && !stmt->istemporal)
 							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);
 
@@ -1029,7 +1047,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1173,6 +1191,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+	if (stmt->istemporal)
+		constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1841,6 +1861,91 @@ CheckPredicate(Expr *predicate)
 				 errmsg("functions in index predicate must be marked IMMUTABLE")));
 }
 
+/*
+ * get_index_attr_temporal_operator
+ *
+ * Finds an operator for a temporal index attribute.
+ * We need an equality operator for normal keys
+ * and an overlaps operator for the range.
+ * Returns the operator oid and strategy in opid and strat,
+ * respectively.
+ */
+static void
+get_index_attr_temporal_operator(Oid opclass,
+								 Oid atttype,
+								 bool isoverlaps,
+								 Oid *opid,
+								 int *strat)
+{
+	Oid opfamily;
+	const char *opname;
+
+	opfamily = get_opclass_family(opclass);
+	/*
+	 * If we have a range type, fall back on anyrange.
+	 * This seems like a hack
+	 * but I can't find any existing lookup function
+	 * that knows about pseudotypes.
+	 * compatible_oper is close but wants a *name*,
+	 * and the point here is to avoid hardcoding a name
+	 * (although it should always be = and &&).
+	 *
+	 * In addition for the normal key elements
+	 * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
+	 * If you're using btree_gist then you'll need the latter.
+	 */
+	if (isoverlaps)
+	{
+		*strat = RTOverlapStrategyNumber;
+		opname = "overlaps";
+		*opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+		if (!OidIsValid(*opid) && type_is_range(atttype))
+			*opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+	}
+	else
+	{
+		*strat = RTEqualStrategyNumber;
+		opname = "equality";
+		*opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+		if (!OidIsValid(*opid) && type_is_range(atttype))
+			*opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+
+		if (!OidIsValid(*opid))
+		{
+			*strat = BTEqualStrategyNumber;
+			*opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+			if (!OidIsValid(*opid) && type_is_range(atttype))
+				*opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *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))));
+	}
+}
+
 /*
  * Compute per-index-column information, including indexed column numbers
  * or index expressions, opclasses and their options. Note, all output vectors
@@ -1863,6 +1968,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool istemporal,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1886,6 +1992,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;
 
+	/* exclusionOpNames can be non-NIL if we are creating a partition */
+	if (istemporal && 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);
 
@@ -2161,6 +2275,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 			indexInfo->ii_ExclusionStrats[attn] = strat;
 			nextExclOp = lnext(exclusionOpNames, nextExclOp);
 		}
+		else if (istemporal)
+		{
+			int strat;
+			Oid opid;
+			get_index_attr_temporal_operator(opclassOids[attn],
+											 atttype,
+											 attn == nkeycols - 1,	/* TODO: Don't assume it's last? */
+											 &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
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8a2c671b66..ba2a8914e4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,6 +208,7 @@ typedef struct NewConstraint
 	Oid			refrelid;		/* PK rel, if FOREIGN */
 	Oid			refindid;		/* OID of PK's index, if FOREIGN */
 	Oid			conid;			/* OID of pg_constraint entry, if FOREIGN */
+	bool		contemporal;	/* Whether the new constraint is temporal */
 	Node	   *qual;			/* Check expr or CONSTR_FOREIGN Constraint */
 	ExprState  *qualstate;		/* Execution state for CHECK expr */
 } NewConstraint;
@@ -10003,6 +10004,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
+									  false,	/* conTemporal */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10301,6 +10303,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conTemporal */
 									  false);
 
 			/*
@@ -10806,6 +10809,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conTemporal */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -11480,6 +11484,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 			newcon->refrelid = con->confrelid;
 			newcon->refindid = con->conindid;
 			newcon->conid = con->oid;
+			newcon->contemporal = con->contemporal;
 			newcon->qual = (Node *) fkconstraint;
 
 			/* Find or create work queue entry for this table */
@@ -11646,10 +11651,12 @@ 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.  If this is a temporal primary key,
+ *	also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
  *
  *	All parameters except pkrel are output parameters.  Also, the function
- *	return value is the number of attributes in the primary key.
+ *	return value is the number of attributes in the primary key,
+ *	not including the WITHOUT OVERLAPS if any.
  *
  *	Used when the column list in the REFERENCES specification is omitted.
  */
@@ -14191,7 +14198,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 	if (CheckIndexCompatible(oldId,
 							 stmt->accessMethod,
 							 stmt->indexParams,
-							 stmt->excludeOpNames))
+							 stmt->excludeOpNames,
+							 stmt->istemporal))
 	{
 		Relation	irel = index_open(oldId, NoLock);
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 52177759ab..0577b60415 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, /* contemporal */
 											  isInternal);	/* is_internal */
 	}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 5e97606793..6d0e946684 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,	/* contemporal */
 							  false);	/* is_internal */
 	if (constrAddr)
 		ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d2032885e..3ea4acb74d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,7 +523,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableElement TypedTableElement ConstraintElem TableFuncElement
 %type <node>	columnDef columnOptions
 %type <defelt>	def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node>	def_arg columnElem where_clause where_or_current_clause
+%type <node>	def_arg columnElem without_overlaps_clause
+				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
@@ -4096,7 +4097,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 without_overlaps_clause ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4105,11 +4106,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;
@@ -4130,7 +4132,7 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
-			| PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+			| PRIMARY KEY '(' columnList without_overlaps_clause ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4138,11 +4140,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;
@@ -4220,6 +4223,11 @@ columnList:
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
 		;
 
+without_overlaps_clause:
+			',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+			| /*EMPTY*/               { $$ = NULL; }
+	;
+
 columnElem: ColId
 				{
 					$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 55c315f0e2..e195410c23 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,6 +124,8 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
 static void transformIndexConstraints(CreateStmtContext *cxt);
 static IndexStmt *transformIndexConstraint(Constraint *constraint,
 										   CreateStmtContext *cxt);
+static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname,
+							   Oid *typid);
 static void transformExtendedStatistics(CreateStmtContext *cxt);
 static void transformFKConstraints(CreateStmtContext *cxt,
 								   bool skipValidation,
@@ -1725,6 +1727,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	index->unique = idxrec->indisunique;
 	index->nulls_not_distinct = idxrec->indnullsnotdistinct;
 	index->primary = idxrec->indisprimary;
+	index->istemporal = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
 	index->transformed = true;	/* don't need transformIndexStmt */
 	index->concurrent = false;
 	index->if_not_exists = false;
@@ -1774,7 +1777,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			nElems;
 				int			i;
 
-				Assert(conrec->contype == CONSTRAINT_EXCLUSION);
+				Assert(conrec->contype == CONSTRAINT_EXCLUSION ||
+						(index->istemporal &&
+						 (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);
@@ -2314,6 +2319,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	}
 	index->nulls_not_distinct = constraint->nulls_not_distinct;
 	index->isconstraint = true;
+	index->istemporal = constraint->without_overlaps != NULL;
 	index->deferrable = constraint->deferrable;
 	index->initdeferred = constraint->initdeferred;
 
@@ -2406,6 +2412,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),
@@ -2682,6 +2693,65 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				notnullcmds = lappend(notnullcmds, notnullcmd);
 			}
 		}
+
+		/*
+		 * Anything in without_overlaps should be included,
+		 * but with the overlaps operator (&&) instead of equality.
+		 */
+		if (constraint->without_overlaps != NULL) {
+			char *without_overlaps_str = strVal(constraint->without_overlaps);
+			IndexElem *iparam = makeNode(IndexElem);
+			char   *typname;
+			Oid		typid;
+
+			/*
+			 * Iterate through the table's columns
+			 * (like just a little bit above).
+			 * If we find one whose name is the same as without_overlaps,
+			 * validate that it's a range type.
+			 *
+			 * Otherwise report an error.
+			 */
+
+			if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid))
+			{
+				if (!type_is_range(typid))
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type",
+									without_overlaps_str)));
+			}
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+						 errmsg("range or PERIOD \"%s\" in WITHOUT OVERLAPS does not exist",
+								without_overlaps_str)));
+
+			iparam->name = pstrdup(without_overlaps_str);
+			iparam->expr = NULL;
+			iparam->indexcolname = NULL;
+			iparam->collation = NIL;
+			iparam->opclass = NIL;
+			iparam->opclassopts = NIL;
+			iparam->ordering = SORTBY_DEFAULT;
+			iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+			index->indexParams = lappend(index->indexParams, iparam);
+
+			index->accessMethod = "gist";
+			constraint->access_method = "gist";
+
+			if (constraint->contype == CONSTR_PRIMARY)
+			{
+				/*
+				 * Force the column to NOT NULL since it is part of the primary key.
+				 */
+				AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
+
+				notnullcmd->subtype = AT_SetAttNotNull;
+				notnullcmd->name = pstrdup(without_overlaps_str);
+				notnullcmds = lappend(notnullcmds, notnullcmd);
+			}
+		}
 	}
 
 	/*
@@ -2801,6 +2871,58 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	return index;
 }
 
+/*
+ * Tries to find a column by name among the existing ones (if it's an ALTER TABLE)
+ * and the new ones. Sets typname and typid if one is found. Returns false if we
+ * couldn't find a match.
+ */
+static bool
+findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid)
+{
+	/* Check the new columns first in case their type is changing. */
+
+	ColumnDef  *column = NULL;
+	ListCell   *columns;
+
+	foreach(columns, cxt->columns)
+	{
+		column = lfirst_node(ColumnDef, columns);
+		if (strcmp(column->colname, colname) == 0)
+		{
+			*typid = typenameTypeId(NULL, column->typeName);
+			*typname = TypeNameToString(column->typeName);
+			return true;
+		}
+	}
+
+	/* Look up columns on existing table. */
+
+	if (cxt->isalter)
+	{
+		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)
+				continue;
+
+			attname = NameStr(attr->attname);
+			if (strcmp(attname, colname) == 0)
+			{
+				Type type = typeidType(attr->atttypid);
+				*typid = attr->atttypid;
+				*typname = pstrdup(typeTypeName(type));
+				ReleaseSysCache(type);
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
 /*
  * transformExtendedStatistics
  *     Handle extended statistic objects
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 97b0ef22ac..778d3e0334 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,8 +338,8 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext,
 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);
+static int	decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+										 bool withoutOverlaps, 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,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				decompile_column_index_array(val, conForm->conrelid, &buf);
+				decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
 
 				/* add foreign relation name */
 				appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2258,7 @@ 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->conindid, false, &buf);
 
 				appendStringInfoChar(&buf, ')');
 
@@ -2344,7 +2344,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, InvalidOid, false, &buf);
 					appendStringInfoChar(&buf, ')');
 				}
 
@@ -2357,6 +2357,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				Oid			indexId;
 				int			keyatts;
 				HeapTuple	indtup;
+				bool		isnull;
 
 				/* Start off the constraint definition */
 				if (conForm->contype == CONSTRAINT_PRIMARY)
@@ -2379,7 +2380,14 @@ 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);
+				/*
+				 * If it has exclusion-style operator OIDs
+				 * then it uses WITHOUT OVERLAPS.
+				 */
+				indexId = conForm->conindid;
+				SysCacheGetAttr(CONSTROID, tup,
+						  Anum_pg_constraint_conexclop, &isnull);
+				keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
 
 				appendStringInfoChar(&buf, ')');
 
@@ -2574,8 +2582,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  * of keys.
  */
 static int
-decompile_column_index_array(Datum column_index_array, Oid relId,
-							 StringInfo buf)
+decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+							 bool withoutOverlaps, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2588,11 +2596,14 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
 	for (j = 0; j < nKeys; j++)
 	{
 		char	   *colName;
+		int			colid = DatumGetInt16(keys[j]);
 
-		colName = get_attname(relId, DatumGetInt16(keys[j]), false);
+		colName = get_attname(relId, colid, false);
 
 		if (j == 0)
 			appendStringInfoString(buf, quote_identifier(colName));
+		else if (withoutOverlaps && j == nKeys - 1)
+			appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
 		else
 			appendStringInfo(buf, ", %s", quote_identifier(colName));
 	}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7234cb3da6..77a5b25889 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5536,8 +5536,9 @@ 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'
+ * associated exclusion constraint or temporal primary key/unique
+ * 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.
  */
@@ -5603,7 +5604,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->contemporal && (
+							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 f7b6176692..efe8d72b32 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6972,7 +6972,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_tablespace,
 				i_indreloptions,
 				i_indstatcols,
-				i_indstatvals;
+				i_indstatvals,
+				i_withoutoverlaps;
 
 	/*
 	 * We want to perform just one query against pg_index.  However, we
@@ -7047,10 +7048,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.conexclop IS NOT NULL AS withoutoverlaps ");
+	else
+		appendPQExpBufferStr(query,
+							 "null AS withoutoverlaps ");
 
 	/*
 	 * The point of the messy-looking outer join is to find a constraint that
@@ -7125,6 +7133,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_indreloptions = PQfnumber(res, "indreloptions");
 	i_indstatcols = PQfnumber(res, "indstatcols");
 	i_indstatvals = PQfnumber(res, "indstatvals");
+	i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");
 
 	indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
 
@@ -7227,6 +7236,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
 				constrinfo->conislocal = true;
 				constrinfo->separate = true;
+				constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
 			}
@@ -16922,9 +16932,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 					break;
 				attname = getAttrName(indkey, tbinfo);
 
-				appendPQExpBuffer(q, "%s%s",
-								  (k == 0) ? "" : ", ",
-								  fmtId(attname));
+				if (k == 0)
+				{
+					appendPQExpBuffer(q, "%s",
+										fmtId(attname));
+				}
+				else if (k == indxinfo->indnkeyattrs - 1 &&
+						coninfo->withoutoverlaps)
+				{
+					appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
+									  fmtId(attname));
+				}
+				else
+				{
+					appendPQExpBuffer(q, ", %s",
+										fmtId(attname));
+				}
 			}
 
 			if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9036b13f6a..773a2de18b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -489,6 +489,7 @@ typedef struct _constraintInfo
 	bool		condeferred;	/* true if constraint is INITIALLY DEFERRED */
 	bool		conislocal;		/* true if constraint has local definition */
 	bool		separate;		/* true if must dump as separate item */
+	bool		withoutoverlaps;	/* true if the last elem is WITHOUT OVERLAPS */
 } ConstraintInfo;
 
 typedef struct _procLangInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 0758fe5ea0..22aa87d3dc 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1015,6 +1015,52 @@ my %tests = (
 		},
 	},
 
+	'ALTER TABLE ONLY test_table_tpk ADD 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,
+			exclude_test_table => 1,
+		},
+		unlike => {
+			only_dump_test_table     => 1,
+			exclude_dump_test_schema => 1,
+			only_dump_measurement    => 1,
+		},
+	},
+
+	'ALTER TABLE ONLY test_table_tuq ADD 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,
+			exclude_test_table => 1,
+		},
+		unlike => {
+			only_dump_test_table     => 1,
+			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 bac94a338c..4fbfc07460 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2384,6 +2384,10 @@ describeOneTableDetails(const char *schemaname,
 			else
 				appendPQExpBufferStr(&buf, ", false AS indisreplident");
 			appendPQExpBufferStr(&buf, ", c2.reltablespace");
+			if (pset.sversion >= 160000)
+				appendPQExpBufferStr(&buf, ", con.contemporal");
+			else
+				appendPQExpBufferStr(&buf, ", false AS contemporal");
 			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"
@@ -2405,8 +2409,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 temporal PK/UNIQUE constraint,
+					 * 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/catalog/index.h b/src/include/catalog/index.h
index 4d8ba81f90..a604546f48 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -91,6 +91,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_TEMPORAL		(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 9b9d8faf35..786a9ca904 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 and foreign keys, signifies the last column is a range
+	 * and should use overlaps instead of equals.
+	 */
+	bool		contemporal;
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -146,7 +152,7 @@ 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 temporal primary keys.
 	 */
 	Oid			conexclop[1] BKI_LOOKUP(pg_operator);
 
@@ -236,6 +242,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
+								  bool conTemporal,
 								  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 179eb9901f..f10886f6ca 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -44,7 +44,8 @@ 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 istemporal);
 extern Oid	GetDefaultOpClass(Oid type_id, Oid am_id);
 extern Oid	ResolveOpClass(const List *opclass, Oid attrType,
 						   const char *accessMethodName, Oid accessMethodId);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cb714f4a19..35c22c37c7 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -155,6 +155,7 @@ typedef struct ExprState
  *		UniqueProcs
  *		UniqueStrats
  *		Unique				is it a unique index?
+ *		Temporal			is it for a temporal constraint?
  *		OpclassOptions		opclass-specific options, or NULL if none
  *		ReadyForInserts		is it valid for inserts?
  *		CheckedUnchanged	IndexUnchanged status determined yet?
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fef4c714b8..0747b54b26 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2641,6 +2641,9 @@ typedef struct Constraint
 	Oid			old_pktable_oid;	/* pg_constraint.confrelid of my former
 									 * self */
 
+	/* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
+	Node	   *without_overlaps; /* String node naming range column */
+
 	/* Fields used for constraints that allow a NOT VALID specification */
 	bool		skip_validation;	/* skip validation of existing rows? */
 	bool		initially_valid;	/* mark the new constraint as valid? */
@@ -3242,6 +3245,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		istemporal;		/* is it for a temporal pkey? */
 	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 0000000000..5e126df071
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,359 @@
+-- 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:  syntax error at or near "WITHOUT"
+LINE 3:  CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OV...
+                                                          ^
+-- 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:  range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+	id INTEGER,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range 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:
+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)
+
+DROP TABLE temporal_rng2;
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng2 (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR:  syntax error at or near "WITHOUT"
+LINE 3:  CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLA...
+                                                      ^
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng2 (
+	id INTEGER,
+	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng2 (
+	id INTEGER,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range type
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng2 (
+	-- 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_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+             Table "public.temporal_rng2"
+  Column  |   Type    | Collation | Nullable | Default 
+----------+-----------+-----------+----------+---------
+ id       | int4range |           |          | 
+ valid_at | tsrange   |           |          | 
+Indexes:
+    "temporal_rng2_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_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_rng2_uq';
+                                 pg_get_indexdef                                 
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
+(1 row)
+
+-- 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;
+--
+-- 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;
+-- 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 4df9d8503b..66b75fa12f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 0000000000..3c3236618c
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,262 @@
+-- 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 INTEGER,
+	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:
+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';
+DROP TABLE temporal_rng2;
+
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+	id int4range,
+	valid_at textrange2,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng2 (
+	valid_at tsrange,
+	CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng2 (
+	id INTEGER,
+	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng2 (
+	id INTEGER,
+	valid_at TEXT,
+	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng2 (
+	-- 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_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+
+-- 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;
+
+--
+-- 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;
+
+-- 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.25.1

From a8a22440f09de9b45a58f122a8e3faef5d279362 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v14 2/5] 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.
---
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 src/backend/commands/tablecmds.c              | 758 +++++++++++-------
 src/backend/parser/gram.y                     |  37 +-
 src/backend/utils/adt/ri_triggers.c           | 341 +++++++-
 src/backend/utils/adt/ruleutils.c             |  19 +-
 src/include/catalog/pg_constraint.h           |   8 +-
 src/include/catalog/pg_proc.dat               |  24 +
 src/include/nodes/parsenodes.h                |   2 +
 src/include/parser/kwlist.h                   |   1 +
 .../regress/expected/without_overlaps.out     | 447 +++++++++++
 src/test/regress/sql/without_overlaps.sql     | 447 +++++++++++
 11 files changed, 1783 insertions(+), 343 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 87f0aab13b..fea8fc3c7a 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">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</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 ]
@@ -1149,8 +1149,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>
@@ -1161,11 +1161,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
@@ -1237,7 +1255,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
          <para>
           Delete any rows referencing the deleted row, or update the
           values of the referencing column(s) to the new values of the
-          referenced columns, respectively.
+          referenced columns, respectively. In a temporal foreign key,
+          the delete will use <literal>FOR PORTION OF</literal> semantics
+          to constrain the effect to the bounds of the referenced row
+          being deleted.
          </para>
         </listitem>
        </varlistentry>
@@ -1250,6 +1271,12 @@ 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
+          of the referenced row.
+         </para>
         </listitem>
        </varlistentry>
 
@@ -1262,6 +1289,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           can only be specified for <literal>ON DELETE</literal> actions.
           (There must be a row in the referenced table matching the default
           values, if they are not null, or the operation will fail.)
+          In a temporal foreign key, the change will use <literal>FOR PORTION
+          OF</literal> semantics to constrain the effect to the bounds of the
+          referenced row.
          </para>
         </listitem>
        </varlistentry>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ba2a8914e4..5af72ea627 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -380,17 +380,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);
@@ -500,7 +503,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);
@@ -510,7 +514,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);
@@ -542,6 +548,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);
@@ -5822,7 +5834,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 
 				validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
 											 con->refindid,
-											 con->conid);
+											 con->conid,
+											 con->contemporal);
 
 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9438,6 +9451,11 @@ 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 = (fkconstraint->fk_period != NULL);
+	int16		pkperiodattnums[1] = {0};
+	int16		fkperiodattnums[1] = {0};
+	Oid			pkperiodtypoids[1] = {0};
+	Oid			fkperiodtypoids[1] = {0};
 	int			i;
 	int			numfks,
 				numpks,
@@ -9532,6 +9550,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfks = transformColumnNameList(RelationGetRelid(rel),
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
+	if (is_temporal)
+		transformColumnNameList(RelationGetRelid(rel),
+							  list_make1(fkconstraint->fk_period),
+							  fkperiodattnums, fkperiodtypoids);
 
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
@@ -9550,7 +9572,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
 											&fkconstraint->pk_attrs,
+											&fkconstraint->pk_period,
 											pkattnum, pktypoid,
+											pkperiodattnums, pkperiodtypoids,
 											opclasses);
 	}
 	else
@@ -9558,8 +9582,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		numpks = transformColumnNameList(RelationGetRelid(pkrel),
 										 fkconstraint->pk_attrs,
 										 pkattnum, pktypoid);
+		if (is_temporal)
+			transformColumnNameList(RelationGetRelid(pkrel),
+									list_make1(fkconstraint->pk_period),
+									pkperiodattnums, pkperiodtypoids);
+
 		/* Look for an index matching the column list */
 		indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+										   is_temporal, pkperiodattnums[0],
 										   opclasses);
 	}
 
@@ -9618,187 +9648,27 @@ 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);
-
-		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)))
-			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)
-		{
-			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));
-		}
+		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] = pkperiodattnums[0];
+		pktypoid[numpks] = pkperiodtypoids[0];
+		fkattnum[numpks] = fkperiodattnums[0];
+		fktypoid[numpks] = fkperiodtypoids[0];
 
-		pfeqoperators[i] = pfeqop;
-		ppeqoperators[i] = ppeqop;
-		ffeqoperators[i] = ffeqop;
+		FindFKComparisonOperators(
+				fkconstraint, tab, numpks, fkattnum,
+				&old_check_ok, &old_pfeqop_item,
+				pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks],
+				is_temporal, true,
+				&pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+		numfks += 1;
+		numpks += 1;
 	}
 
 	/*
@@ -9817,7 +9687,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,
@@ -9833,7 +9704,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.
@@ -9918,7 +9790,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;
@@ -10004,7 +9877,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
-									  false,	/* conTemporal */
+									  is_temporal,	/* conTemporal */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10080,7 +9953,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);
@@ -10138,7 +10012,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;
@@ -10186,6 +10061,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 			newcon->refrelid = RelationGetRelid(pkrel);
 			newcon->refindid = indexOid;
 			newcon->conid = parentConstr;
+			newcon->contemporal = fkconstraint->fk_period != NULL;
 			newcon->qual = (Node *) fkconstraint;
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10303,7 +10179,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conTemporal */
+									  is_temporal,	/* conTemporal */
 									  false);
 
 			/*
@@ -10334,7 +10210,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10570,7 +10447,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->contemporal);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10809,7 +10687,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
-								  false,	/* conTemporal */
+								  constrForm->contemporal,	/* conTemporal */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -10843,13 +10721,232 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								constrForm->contemporal);
 		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;
+	int16		eqstrategy;
+	Oid			pfeqop_right;
+
+	/* 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");
+		eqstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
+	}
+	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;
+}
+
 /*
  * 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
@@ -11595,7 +11692,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11663,7 +11759,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;
@@ -11728,36 +11826,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;
@@ -11784,6 +11901,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")));
 	}
 
 	/*
@@ -11805,12 +11926,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))
@@ -11848,6 +11973,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
@@ -11957,7 +12095,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -11986,8 +12125,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;
 
 	/*
@@ -12056,6 +12197,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
@@ -12075,12 +12217,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;
 	}
 
@@ -12138,37 +12286,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,
@@ -12198,37 +12377,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/parser/gram.y b/src/backend/parser/gram.y
index 3ea4acb74d..7d22319d40 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,11 +523,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableElement TypedTableElement ConstraintElem TableFuncElement
 %type <node>	columnDef columnOptions
 %type <defelt>	def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node>	def_arg columnElem without_overlaps_clause
+%type <node>	def_arg columnElem without_overlaps_clause optionalPeriodName
 				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
 %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
 
@@ -4186,21 +4187,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);
@@ -4228,6 +4231,16 @@ without_overlaps_clause:
 			| /*EMPTY*/               { $$ = NULL; }
 	;
 
+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);
@@ -17515,6 +17528,7 @@ reserved_keyword:
 			| ONLY
 			| OR
 			| ORDER
+			| PERIOD
 			| PLACING
 			| PRIMARY
 			| REFERENCES
@@ -17824,6 +17838,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 6945d99b3d..365b844bf3 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -31,6 +31,7 @@
 #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/trigger.h"
 #include "executor/executor.h"
@@ -48,6 +49,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,6 +120,7 @@ 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 */
@@ -200,8 +203,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);
 
@@ -361,26 +365,57 @@ 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 +425,8 @@ RI_FKey_check(TriggerData *trigdata)
 			queryoids[i] = fk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->temporal)
+			appendStringInfo(&querybuf, ") x1 HAVING $%d <@ pg_catalog.range_agg(x1.r)", riinfo->nkeys);
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -497,21 +534,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 +586,8 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 			queryoids[i] = pk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->temporal)
+			appendStringInfo(&querybuf, ") x1 HAVING $%d <@ pg_catalog.range_agg(x1.r)", riinfo->nkeys);
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -695,10 +762,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 +1286,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 +1434,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 +1533,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 +1681,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 +2332,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 +2367,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	riinfo->confupdtype = conForm->confupdtype;
 	riinfo->confdeltype = conForm->confdeltype;
 	riinfo->confmatchtype = conForm->confmatchtype;
+	riinfo->temporal = conForm->contemporal;
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2791,9 +2988,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 +3001,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 +3034,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,10 +3272,16 @@ 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;
 	}
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 778d3e0334..1db5ed557b 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, Oid indexId,
-										 bool withoutOverlaps, StringInfo buf);
+										 bool withoutOverlaps, 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,10 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
+				/* If it is a temporal foreign key then it uses PERIOD. */
+				decompile_column_index_array(val, conForm->conrelid,
+											 conForm->conindid, false,
+											 conForm->contemporal, &buf);
 
 				/* add foreign relation name */
 				appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2261,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_confkey);
 
-				decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
+				decompile_column_index_array(val, conForm->confrelid,
+											 conForm->conindid, false,
+											 conForm->contemporal, &buf);
 
 				appendStringInfoChar(&buf, ')');
 
@@ -2344,7 +2349,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (!isnull)
 				{
 					appendStringInfoString(&buf, " (");
-					decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
+					decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, false, &buf);
 					appendStringInfoChar(&buf, ')');
 				}
 
@@ -2387,7 +2392,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				indexId = conForm->conindid;
 				SysCacheGetAttr(CONSTROID, tup,
 						  Anum_pg_constraint_conexclop, &isnull);
-				keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
+				keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, false, &buf);
 
 				appendStringInfoChar(&buf, ')');
 
@@ -2583,7 +2588,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  */
 static int
 decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
-							 bool withoutOverlaps, StringInfo buf)
+							 bool withoutOverlaps, bool withPeriod, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2604,6 +2609,8 @@ decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
 			appendStringInfoString(buf, quote_identifier(colName));
 		else if (withoutOverlaps && j == nKeys - 1)
 			appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
+		else if (withPeriod && j == nKeys - 1)
+			appendStringInfo(buf, ", PERIOD %s", quote_identifier(colName));
 		else
 			appendStringInfo(buf, ", %s", quote_identifier(colName));
 	}
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 786a9ca904..7ca0534b48 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);
@@ -180,7 +180,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 9805bc6118..3132468a9c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3942,6 +3942,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/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0747b54b26..479b5ffa35 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2632,7 +2632,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 5984dcfa4b..ccf5bf8aa6 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/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 5e126df071..e0c37ac601 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -357,3 +357,450 @@ 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 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
+--
+-- 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);
+-- 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".
+--
+-- 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 3c3236618c..8a03b14961 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -260,3 +260,450 @@ 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 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)
+);
+
+--
+-- 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);
+
+-- 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]';
+
+--
+-- 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.25.1

From d2b7da7fecab992a0fb20337d01ea4f15f16aceb Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v14 3/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. 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.
---
 doc/src/sgml/ref/delete.sgml                  |  46 ++
 doc/src/sgml/ref/update.sgml                  |  47 ++
 doc/src/sgml/trigger.sgml                     |  60 ++-
 src/backend/commands/tablecmds.c              |   1 +
 src/backend/commands/trigger.c                |  49 ++
 src/backend/executor/execMain.c               |   1 +
 src/backend/executor/nodeModifyTable.c        | 222 ++++++++-
 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                  | 214 ++++++++-
 src/backend/parser/gram.y                     |  47 +-
 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/tcop/utility.c                    |   2 +-
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/period.c                |  56 +++
 src/backend/utils/adt/rangetypes.c            |  42 ++
 src/backend/utils/adt/ri_triggers.c           |  74 +++
 src/backend/utils/cache/lsyscache.c           |  52 +++
 src/include/commands/trigger.h                |   1 +
 src/include/nodes/execnodes.h                 |  27 ++
 src/include/nodes/parsenodes.h                |  44 +-
 src/include/nodes/pathnodes.h                 |   1 +
 src/include/nodes/plannodes.h                 |   1 +
 src/include/nodes/primnodes.h                 |  25 +
 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/include/utils/period.h                    |  19 +
 src/include/utils/rangetypes.h                |   3 +
 src/test/regress/expected/for_portion_of.out  | 431 ++++++++++++++++++
 src/test/regress/expected/privileges.out      |  18 +
 src/test/regress/expected/updatable_views.out |  32 ++
 .../regress/expected/without_overlaps.out     | 129 +++++-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/for_portion_of.sql       | 340 ++++++++++++++
 src/test/regress/sql/privileges.sql           |  18 +
 src/test/regress/sql/updatable_views.sql      |  14 +
 src/test/regress/sql/without_overlaps.sql     |  77 +++-
 47 files changed, 2157 insertions(+), 50 deletions(-)
 create mode 100644 src/backend/utils/adt/period.c
 create mode 100644 src/include/utils/period.h
 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/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml
index 1b81b4e7d7..c0903ec361 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,15 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <para>
+   If the table has a <link linkend="ddl-periods-application-periods">range column
+   or <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 new rows spanning the "cut
+   off" duration will be inserted to preserve the old values.
+  </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 +126,42 @@ 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. This
+      must match the range or period used in the table's temporal primary key.
+     </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>. It may also
+      be the special value <literal>MINVALUE</literal> to indicate a delete whose
+      beginning is unbounded.
+     </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>. It may also
+      be the special value <literal>MAXVALUE</literal> to indicate a delete whose
+      end is unbounded.
+     </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 2ab24b0523..069cb3ca5c 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> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</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,16 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
+   If the table has a <link linkend="ddl-periods-application-periods">range column
+   or <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 it will be truncated to fit
+   within the bounds, and new rows spanning the "cut off" duration will be
+   inserted to preserve the old values.
+  </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 +125,42 @@ 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. This
+      must match the range or period used in the table's temporal primary key.
+     </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>. It may also
+      be the special value <literal>MINVALUE</literal> to indicate an update whose
+      beginning is unbounded.
+     </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>. It may also
+      be the special value <literal>MAXVALUE</literal> to indicate an update whose
+      end is unbounded.
+     </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 6e1f370b21..b58e6348ad 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -537,17 +537,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>
 
@@ -815,6 +816,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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5af72ea627..5a4d7ff273 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12168,6 +12168,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 0577b60415..2f05557b34 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];
@@ -3008,6 +3013,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++)
@@ -3157,6 +3163,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);
 
@@ -3623,6 +3630,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;
@@ -3896,6 +3904,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);

@@ -4105,6 +4114,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;
 	}
@@ -4469,6 +4479,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);
 
@@ -6005,6 +6016,43 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
+ * Copies 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()
  *
@@ -6420,6 +6468,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 4c5a7bbf62..69096cbefb 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 5005d8c0d1..92d6611f51 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -61,6 +61,9 @@
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/memutils.h"
+#include "utils/period.h"
+#include "utils/rangetypes.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"

@@ -141,6 +144,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 +1212,138 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+/* ----------------------------------------------------------------
+ *		set_leftover_tuple_bounds
+ *
+ *		Saves the new bounds into the range attribute
+ * ----------------------------------------------------------------
+ */
+static void set_leftover_tuple_bounds(TupleTableSlot *leftoverTuple,
+									  ForPortionOfExpr *forPortionOf,
+									  TypeCacheEntry *typcache,
+									  RangeType *leftoverRangeType)
+{
+	/* Store the range directly */
+
+	leftoverTuple->tts_values[forPortionOf->rangeVar->varattno - 1] = RangeTypePGetDatum(leftoverRangeType);
+	leftoverTuple->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+}
+
+/* ----------------------------------------------------------------
+ *		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)
+{
+	ModifyTableState *mtstate = context->mtstate;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+	Datum oldRange;
+	RangeType *oldRangeType;
+	RangeType *targetRangeType;
+	RangeType *leftoverRangeType1;
+	RangeType *leftoverRangeType2;
+	Oid rangeTypeOid = forPortionOf->rangeType;
+	bool isNull = false;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+	TupleTableSlot *leftoverTuple1 = fpoState->fp_Leftover1;
+	TupleTableSlot *leftoverTuple2 = fpoState->fp_Leftover2;
+
+	/*
+	 * 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");
+
+	oldRange = slot_getattr(oldtupleSlot, forPortionOf->rangeVar->varattno, &isNull);
+
+	if (isNull)
+		elog(ERROR, "found a NULL range in a temporal table");
+	oldRangeType = DatumGetRangeTypeP(oldRange);
+
+	/* Get the target range. */
+
+	targetRangeType = DatumGetRangeTypeP(fpoState->fp_targetRange);
+
+	/*
+	 * Get the range's type cache entry. This is worth caching for the whole UPDATE
+	 * like range functions do.
+	 */
+
+	typcache = fpoState->fp_rangetypcache;
+	if (typcache == NULL)
+	{
+		typcache = lookup_type_cache(rangeTypeOid, TYPECACHE_RANGE_INFO);
+		if (typcache->rngelemtype == NULL)
+			elog(ERROR, "type %u is not a range type", rangeTypeOid);
+		fpoState->fp_rangetypcache = typcache;
+	}
+
+	/* Get the ranges to the left/right of the targeted range. */
+
+	range_leftover_internal(typcache, oldRangeType, targetRangeType, &leftoverRangeType1,
+			&leftoverRangeType2);
+
+	/*
+	 * Insert a copy of the tuple with the lower leftover range.
+	 * Even if the table is partitioned,
+	 * our insert won't extend past the current row, so we don't need to re-route.
+	 * TODO: Really? What if you update the partition key?
+	 */
+
+	if (!RangeIsEmpty(leftoverRangeType1))
+	{
+		// TODO: anything we need to clear here?
+		// Are we in the row context?
+		HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+		ExecForceStoreHeapTuple(oldtuple, leftoverTuple1, false);
+
+		set_leftover_tuple_bounds(leftoverTuple1, forPortionOf, typcache, leftoverRangeType1);
+		ExecMaterializeSlot(leftoverTuple1);
+
+		// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+		ExecInsert(context, resultRelInfo, leftoverTuple1, node->canSetTag, NULL, NULL);
+	}
+
+	/*
+	 * Insert a copy of the tuple with the upper leftover range
+	 * Even if the table is partitioned,
+	 * our insert won't extend past the current row, so we don't need to re-route.
+	 * TODO: Really? What if you update the partition key?
+	 */
+
+	if (!RangeIsEmpty(leftoverRangeType2))
+	{
+		HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+		ExecForceStoreHeapTuple(oldtuple, leftoverTuple2, false);
+
+		set_leftover_tuple_bounds(leftoverTuple2, forPortionOf, typcache, leftoverRangeType2);
+		ExecMaterializeSlot(leftoverTuple2);
+
+		// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+		ExecInsert(context, resultRelInfo, leftoverTuple2, node->canSetTag, NULL, NULL);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1357,7 +1496,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 +1530,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);
@@ -2133,6 +2278,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,
@@ -3610,6 +3760,7 @@ ExecModifyTable(PlanState *pstate)
 		 * Reset per-tuple memory context used for processing on conflict and
 		 * returning clauses, to free any expression evaluation storage
 		 * allocated in the previous cycle.
+		 * TODO: It sounds like FOR PORTION OF might need to do something here too?
 		 */
 		if (pstate->ps_ExprContext)
 			ResetExprContext(pstate->ps_ExprContext);
@@ -4282,6 +4433,75 @@ 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;
+		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 slots for INSERTing the leftovers */
+
+		fpoState->fp_Leftover1 =
+			ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+		fpoState->fp_Leftover2 =
+			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.
+		 *
+		 * They should all have the same tupledesc, so it's okay
+		 * that any one of them can use the tuple table slots there.
+		 */
+		for (i = 0; i < nrels; i++)
+		{
+			resultRelInfo = &mtstate->resultRelInfo[i];
+			resultRelInfo->ri_forPortionOf = fpoState;
+		}
+
+		/* 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, ie, the
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c03f4f23e2..62ccbf9d3c 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2442,6 +2442,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;
@@ -2582,6 +2590,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))
@@ -3425,6 +3435,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->rangeSet, fpo->rangeSet, List *);
+
+				return (Node *) newnode;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3603,6 +3626,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 34ca6d4ac2..954eb4da83 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 44efb1f4eb..b2d8a67f4c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1969,6 +1969,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 211ba65389..823d1fbab1 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3670,7 +3670,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);
 
@@ -3736,6 +3737,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 7a1dfb6364..c4b6d443cf 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -47,11 +47,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 +62,17 @@
 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 Node *transformForPortionOfBound(Node *n, bool isLowerBound);
+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 +488,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 +535,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
 	qry->commandType = CMD_DELETE;
@@ -550,7 +574,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 +1201,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 +1231,161 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+ * transformForPortionOfBound
+ *    transforms UNBOUNDED pseudo-column references to NULL
+ *    (which represent "unbounded" in a range type, otherwise returns
+ *    its input unchanged.
+ */
+static Node *
+transformForPortionOfBound(Node *n, bool isLowerBound)
+{
+	if (nodeTag(n) == T_ColumnRef)
+	{
+		ColumnRef  *cref = (ColumnRef *) n;
+		char	   *cname = "";
+		A_Const	   *n2;
+
+		if (list_length(cref->fields) == 1 &&
+			IsA(linitial(cref->fields), String))
+			cname = strVal(linitial(cref->fields));
+
+		if (strcmp("unbounded", cname) != 0)
+			return n;
+
+		n2 = makeNode(A_Const);
+		n2->isnull = true;
+		n2->location = ((ColumnRef *)n)->location;
+
+		return (Node *)n2;
+	}
+	else
+		return n;
+}
+
+/*
+ * 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;
+	ForPortionOfExpr *result;
+	List *targetList;
+	Node *target_start, *target_end;
+	Var *rangeVar;
+	FuncCall *fc;
+
+	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->range_name_location)));
+	attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+	// TODO: check attr->attisdropped (?),
+	// and figure out concurrency issues with that in general.
+	// It should work the same as updating any other column.
+
+	/* 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->range_name_location)));
+
+	rangeVar = makeVar(
+			rtindex,
+			range_attno,
+			attr->atttypid,
+			attr->atttypmod,
+			attr->attcollation,
+			0);
+	rangeVar->location = forPortionOf->range_name_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);
+
+
+	/*
+	 * 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.
+	 */
+	target_start = transformForPortionOfBound(forPortionOf->target_start, true);
+	target_end   = transformForPortionOfBound(forPortionOf->target_end, false);
+	fc = makeFuncCall(
+			list_make2(makeString(range_type_namespace), makeString(range_type_name)),
+			list_make2(target_start, target_end),
+			COERCE_EXPLICIT_CALL,
+			forPortionOf->range_name_location);
+	result->targetRange = transformExpr(pstate, (Node *) fc, EXPR_KIND_UPDATE_PORTION);
+
+	/* overlapsExpr is something we can add to the whereClause */
+	result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, "&&",
+			(Node *) copyObject(rangeVar), (Node *) fc,
+			forPortionOf->range_name_location);
+
+	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 = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
+				(Node *) copyObject(rangeVar), (Node *) fc,
+				forPortionOf->range_name_location);
+		TargetEntry *tle;
+
+		targetList = NIL;
+		rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
+		tle = makeTargetEntry(rangeSetExpr,
+							  range_attno,
+							  range_name,
+							  false);
+
+		targetList = lappend(targetList, tle);
+
+		/* Mark the range column as requiring update permissions */
+		target_perminfo->updatedCols = bms_add_member(target_perminfo->updatedCols,
+													  range_attno - FirstLowInvalidHeapAttributeNumber);
+
+		result->rangeSet = targetList;
+	}
+	else
+		result->rangeSet = NIL;
+
+	result->range_name = range_name;
+
+	return result;
+}
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2592,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
 	qry->commandType = CMD_UPDATE;
@@ -2426,6 +2610,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 +2630,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 +2640,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 +2659,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 +2709,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 7d22319d40..5a49ba6fa3 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
@@ -828,6 +830,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %nonassoc	KEYS OBJECT_P SCALAR VALUE_P
 %nonassoc	WITH WITHOUT
 
+/*
+ * 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.
+ */
+%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P
+%nonassoc TO
 /*
  * To support target_el without AS, it used to be necessary to assign IDENT an
  * explicit precedence just less than Op.  While that's not really necessary
@@ -12166,14 +12178,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;
 				}
@@ -12236,6 +12250,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
@@ -12244,10 +12259,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;
 				}
@@ -13683,6 +13699,19 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+for_portion_of_clause:
+			FOR PORTION OF ColId FROM a_expr TO a_expr
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->range_name_location = @4;
+					n->target_start = $6;
+					n->target_end = $8;
+					$$ = n;
+				}
+			| /*EMPTY*/					{ $$ = NULL; }
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -17238,6 +17267,7 @@ unreserved_keyword:
 			| PASSWORD
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -17842,6 +17872,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 85cd47b7ae..ce8e0be045 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -563,6 +563,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
@@ -953,6 +960,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 9f6afc351c..d09b2d2b1c 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 64c582c344..fee6bd902a 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 b3f0b6a137..31ed01a84e 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 91b1156d99..c00802ae23 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 b486ab559a..576a98e0f4 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3473,6 +3473,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->rangeSet)
+		{
+			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
@@ -3811,6 +3835,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->rangeSet)
+				{
+					TargetEntry *tle = (TargetEntry *) lfirst(tl);
+					parsetree->targetList = lappend(parsetree->targetList, tle);
+				}
+			}
+
 			parsetree->targetList =
 				rewriteTargetListIU(parsetree->targetList,
 									parsetree->commandType,
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index e3ccf6c7f7..6781e55020 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1560,7 +1560,7 @@ ProcessUtilitySlow(ParseState *pstate,
 									true,	/* check_rights */
 									true,	/* check_not_in_use */
 									false,	/* skip_build */
-									false); /* quiet */
+									false);	/* quiet */
 
 					/*
 					 * Add the CREATE INDEX node itself to stash right away;
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0de0bbb1b8..a4f226ec8d 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 0000000000..0ed4304e16
--- /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/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index d65e5625c7..d4e1732377 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -1206,6 +1206,48 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 	return false;
 }
 
+/*
+ * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1
+ * after subtracting r2, or if nothing is left then to the empty range.
+ * output1 will always be "before" r2 and output2 "after".
+ */
+void
+range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+						const RangeType *r2, RangeType **output1, RangeType **output2)
+{
+	RangeBound	lower1,
+				lower2;
+	RangeBound	upper1,
+				upper2;
+	bool		empty1,
+				empty2;
+
+	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+	if (range_cmp_bounds(typcache, &lower1, &lower2) < 0)
+	{
+		lower2.inclusive = !lower2.inclusive;
+		lower2.lower = false;
+		*output1 = make_range(typcache, &lower1, &lower2, false, NULL);
+	}
+	else
+	{
+		*output1 = make_empty_range(typcache);
+	}
+
+	if (range_cmp_bounds(typcache, &upper1, &upper2) > 0)
+	{
+		upper2.inclusive = !upper2.inclusive;
+		upper2.lower = true;
+		*output2 = make_range(typcache, &upper2, &upper1, false, NULL);
+	}
+	else
+	{
+		*output2 = make_empty_range(typcache);
+	}
+}
+
 /* range -> range aggregate functions */
 
 Datum
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 365b844bf3..446e898eb6 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -227,6 +227,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,
@@ -235,6 +236,11 @@ 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 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);

 /*
@@ -443,6 +449,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -600,6 +607,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 	result = ri_PerformCheck(riinfo, &qkey, qplan,
 							 fk_rel, pk_rel,
 							 oldslot, NULL,
+							 -1, 0,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -699,6 +707,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = 0;
 
 	riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger,
 									trigdata->tg_relation, true);
@@ -795,9 +805,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);
 
@@ -904,6 +921,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1025,6 +1043,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1257,6 +1276,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2519,6 +2539,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,
@@ -2574,6 +2595,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
@@ -3287,3 +3314,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_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 fc6d267e44..391762f308 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2170,6 +2170,58 @@ 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);
+		return *typnamespace;
+	}
+	else
+		return false;
+}
+
 /*
  * get_typlen
  *
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 430e3ca7dd..b3d90deaa7 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 35c22c37c7..b5a2c22487 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;
@@ -423,6 +425,28 @@ 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_rangetypcache;	/* type cache entry of the range */
+	TupleTableSlot *fp_Existing;		/* slot to store existing target tuple in */
+	TupleTableSlot *fp_Leftover1;		/* slot to store leftover below the target range */
+	TupleTableSlot *fp_Leftover2;		/* slot to store leftover above the target range */
+} ForPortionOfState;
+
 /*
  * ResultRelInfo
  *
@@ -543,6 +567,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 479b5ffa35..177df96c86 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -148,6 +148,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 */
@@ -1537,6 +1540,19 @@ typedef struct RowMarkClause
 	bool		pushedDown;		/* pushed down from higher query level? */
 } RowMarkClause;
 
+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <period-name> FROM <t1> TO <t2>
+ */
+typedef struct ForPortionOfClause
+{
+	NodeTag		type;
+	char	   *range_name;
+	int			range_name_location;
+	Node	   *target_start;
+	Node	   *target_end;
+} ForPortionOfClause;
+
 /*
  * WithClause -
  *	   representation of WITH clause
@@ -1917,12 +1933,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;
 
 /* ----------------------
@@ -1931,13 +1948,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 5702fbba60..d2be7f26d1 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 1b787fe031..2bca7086e2 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 60d72a876b..b3c6f6cecf 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2041,4 +2041,29 @@ 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	   *rangeSet;			/* List of TargetEntrys to set the time column(s) */
+} ForPortionOfExpr;
+
 #endif							/* PRIMNODES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 6e557bebc4..c932d1204c 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 c96483ae78..b6d5065e12 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 ccf5bf8aa6..c89ceea9f6 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 f589112d5e..0d601f34a5 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 f5fdbfe116..a117527d8a 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/include/utils/period.h b/src/include/utils/period.h
new file mode 100644
index 0000000000..0a8af3edb0
--- /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/rangetypes.h b/src/include/utils/rangetypes.h
index 6b420a8618..48ee2debed 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,5 +164,8 @@ 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_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+									const RangeType *r2, RangeType **output1,
+									RangeType **output2);
 
 #endif							/* RANGETYPES_H */
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 0000000000..30919b55e5
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,431 @@
+-- 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,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 = 'foo';
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+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,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 UNBOUNDED
+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 UNBOUNDED
+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 UNBOUNDED;
+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 UNBOUNDED;
+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)
+
+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,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 UNBOUNDED
+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 UNBOUNDED
+                       ^
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+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.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,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 UNBOUNDED
+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 UNBOUNDED
+                                           ^
+-- 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 UNBOUNDED
+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 UNBOUNDED 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 UNBOUNDED 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 UNBOUNDED
+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 UNBOUNDED TO UNBOUNDED
+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^2'
+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^3'
+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)';
+-- 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,2)';
+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,2)';
+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,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) | ["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","Wed Apr 04 00:00:00 2018") | one^4
+(20 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,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) | ["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,4)'
+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_insert
+  BEFORE INSERT ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+  BEFORE UPDATE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+  BEFORE DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+  AFTER 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,6)';
+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,6)';
+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;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index c1e610e62f..cf2c7df900 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 1950e6f281..f4b88bcc38 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 e0c37ac601..f4b1297abd 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -269,6 +269,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
@@ -321,6 +351,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 (
@@ -356,6 +416,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
@@ -542,6 +632,13 @@ 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 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), ["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')
@@ -579,13 +676,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
 --
@@ -607,6 +716,12 @@ 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 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), ["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');
@@ -631,9 +746,19 @@ 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');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
 --
 -- test ON UPDATE/DELETE options
 --
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 66b75fa12f..99cf6f18d6 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 0000000000..bf2ab093a6
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,340 @@
+-- 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,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 = 'foo';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2019-01-15' TO UNBOUNDED;
+
+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,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 UNBOUNDED
+SET name = 'foo';
+SELECT * FROM for_portion_of_test;
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-15' TO UNBOUNDED
+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 UNBOUNDED;
+SELECT * FROM for_portion_of_test;
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid2_at FROM '2018-01-20' TO UNBOUNDED;
+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,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 UNBOUNDED
+SET name = 'foo'
+WHERE id = '[5,6)';
+
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO UNBOUNDED
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+
+-- 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)';
+
+-- 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)';
+
+-- 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)';
+
+-- Setting with a column fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM lower(valid_at) TO UNBOUNDED
+SET name = 'nope'
+WHERE id = '[3,4)';
+
+-- 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 UNBOUNDED
+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 UNBOUNDED 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 UNBOUNDED 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 UNBOUNDED
+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 UNBOUNDED TO UNBOUNDED
+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^2'
+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^3'
+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)';
+
+-- 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,2)';
+
+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,2)';
+
+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,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;
+
+--
+-- 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)';
+
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+
+-- Deleting with 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;
+
+-- 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 *;
+
+-- 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_insert
+  BEFORE INSERT ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+  AFTER INSERT ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+  BEFORE UPDATE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+  AFTER UPDATE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+  BEFORE DELETE ON for_portion_of_test
+  FOR EACH ROW
+  EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+  AFTER 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,6)';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- 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;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index bf0035d96d..366be99a27 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 eaee0b7e1d..f814701a43 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 8a03b14961..6d9ed8781d 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -203,6 +203,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;
 
 --
@@ -241,6 +255,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:
@@ -259,6 +289,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;
 
 --
@@ -440,6 +486,11 @@ 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 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')
@@ -475,13 +526,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
 --
@@ -501,6 +562,10 @@ 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 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');
@@ -525,9 +590,17 @@ 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');
+DELETE FROM temporal_rng WHERE id = '[5,5]';
 
 --
 -- test ON UPDATE/DELETE options
-- 
2.25.1

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

---
 src/backend/commands/tablecmds.c              |  28 +-
 src/backend/utils/adt/ri_triggers.c           | 549 +++++++++++++++++-
 src/include/catalog/pg_proc.dat               |  20 +
 .../regress/expected/without_overlaps.out     | 413 ++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 173 +++++-
 5 files changed, 1155 insertions(+), 28 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5a4d7ff273..b61853fd82 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12303,11 +12303,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",
@@ -12394,11 +12402,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 446e898eb6..edf799fb1d 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -83,6 +83,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)
@@ -190,6 +196,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,
@@ -1390,6 +1397,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 -
  *
@@ -1427,6 +1555,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 -
  *
@@ -2551,8 +3088,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
@@ -2587,8 +3124,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
 	{
@@ -3299,8 +3838,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 3132468a9c..725eca126d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3949,6 +3949,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 => '',
@@ -3957,6 +3963,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 f4b1297abd..92d67c5467 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -148,6 +148,7 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
  CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
 (1 row)
 
+DROP TABLE temporal_rng2;
 -- UNIQUE with two columns plus a range:
 CREATE TABLE temporal_rng3 (
 	id1 int4range,
@@ -771,7 +772,62 @@ ALTER TABLE temporal_fk_rng2rng
 		FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng
 		ON DELETE CASCADE ON UPDATE CASCADE;
-ERROR:  action not supported for temporal foreign keys
+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 = '[4,4]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8)
+ [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8)
+ [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [10,11) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [16,17)
+ [10,11) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [15,16)
+(2 rows)
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [8,9)
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9)
+(2 rows)
+
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+ id | valid_at | parent_id 
+----+----------+-----------
+(0 rows)
+
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [11,12) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [17,18)
+(1 row)
+
 -- 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]');
@@ -781,7 +837,67 @@ ALTER TABLE temporal_fk_rng2rng
 		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
+UPDATE temporal_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_fk_rng2rng WHERE id = '[6,6]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [9,10)
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [9,10)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | 
+ [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | 
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [12,13) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | 
+ [12,13) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [18,19)
+(2 rows)
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [11,12)
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [11,12)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | 
+ [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | 
+ [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | 
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [13,14) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | 
+ [13,14) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [20,21)
+(2 rows)
+
 -- 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'));
@@ -793,7 +909,95 @@ 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
+UPDATE temporal_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_fk_rng2rng WHERE id = '[8,8]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [12,13)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [12,13)
+(3 rows)
+
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+  id   |                        valid_at                         | parent_id 
+-------+---------------------------------------------------------+-----------
+ [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [14,15) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [14,15) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [22,23)
+(2 rows)
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+   id   |                        valid_at                         | parent_id 
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [14,15)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [14,15)
+(3 rows)
+
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+   id   |                        valid_at                         | parent_id 
+--------+---------------------------------------------------------+-----------
+ [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0)
+ [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0)
+(3 rows)
+
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
+   id    |                        valid_at                         | parent_id 
+---------+---------------------------------------------------------+-----------
+ [15,16) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0)
+ [15,16) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [24,25)
+(2 rows)
+
+-- FK with a custom range type
+CREATE TYPE mydaterange AS range(subtype=date);
+CREATE TABLE temporal_rng2 (
+	id int4range,
+	valid_at mydaterange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at mydaterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng2 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk2_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_fk2_rng2rng;
+DROP TABLE temporal_rng2;
+DROP TYPE mydaterange;
 -- FK between partitioned tables
 CREATE TABLE temporal_partitioned_rng (
 	id int4range,
@@ -801,8 +1005,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'),
@@ -815,8 +1019,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]'),
@@ -851,7 +1055,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]';
@@ -863,7 +1067,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]';
@@ -885,7 +1089,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]';
@@ -897,35 +1101,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 6d9ed8781d..1d757e1ea1 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -103,6 +103,7 @@ CREATE TABLE temporal_rng2 (
 \d temporal_rng2
 SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
 SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+DROP TABLE temporal_rng2;
 
 -- UNIQUE with two columns plus a range:
 CREATE TABLE temporal_rng3 (
@@ -615,6 +616,28 @@ ALTER TABLE temporal_fk_rng2rng
 		FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng
 		ON DELETE CASCADE ON UPDATE CASCADE;
+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 = '[4,4]';
+UPDATE temporal_rng SET id = '[7,7]' WHERE id = '[6,6]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[4,4]';
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]');
+UPDATE temporal_rng SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[10,10]';
+
+-- test FK parent deletes CASCADE
+INSERT INTO temporal_rng VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+DELETE FROM temporal_rng WHERE id = '[8,8]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[5,5]';
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]');
+DELETE FROM temporal_rng WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[11,11]';
 
 -- test FK parent updates SET NULL
 INSERT INTO temporal_rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'));
@@ -625,6 +648,28 @@ ALTER TABLE temporal_fk_rng2rng
 		FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng
 		ON DELETE SET NULL ON UPDATE SET NULL;
+UPDATE temporal_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_fk_rng2rng WHERE id = '[6,6]';
+UPDATE temporal_rng SET id = '[10,10]' WHERE id = '[9,9]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[6,6]';
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]');
+UPDATE temporal_rng SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[12,12]';
+
+-- test FK parent deletes SET NULL
+INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+DELETE FROM temporal_rng WHERE id = '[11,11]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[7,7]';
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]');
+DELETE FROM temporal_rng WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[13,13]';
 
 -- test FK parent updates SET DEFAULT
 INSERT INTO temporal_rng VALUES ('[-1,-1]', tsrange(null, null));
@@ -637,6 +682,54 @@ ALTER TABLE temporal_fk_rng2rng
 		FOREIGN KEY (parent_id, PERIOD valid_at)
 		REFERENCES temporal_rng
 		ON DELETE SET DEFAULT ON UPDATE SET DEFAULT;
+UPDATE temporal_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_fk_rng2rng WHERE id = '[8,8]';
+UPDATE temporal_rng SET id = '[13,13]' WHERE id = '[12,12]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[8,8]';
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]');
+UPDATE temporal_rng SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[14,14]';
+
+-- test FK parent deletes SET DEFAULT
+INSERT INTO temporal_rng VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]');
+DELETE FROM temporal_rng FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+DELETE FROM temporal_rng WHERE id = '[14,14]';
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[9,9]';
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01'));
+INSERT INTO temporal_rng VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01'));
+INSERT INTO temporal_fk_rng2rng VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]');
+DELETE FROM temporal_rng WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp;
+SELECT * FROM temporal_fk_rng2rng WHERE id = '[15,15]';
+
+-- FK with a custom range type
+
+CREATE TYPE mydaterange AS range(subtype=date);
+
+CREATE TABLE temporal_rng2 (
+	id int4range,
+	valid_at mydaterange,
+	CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+CREATE TABLE temporal_fk2_rng2rng (
+	id int4range,
+	valid_at mydaterange,
+	parent_id int4range,
+	CONSTRAINT temporal_fk2_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
+	CONSTRAINT temporal_fk2_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
+		REFERENCES temporal_rng2 (id, PERIOD valid_at) ON DELETE CASCADE
+);
+INSERT INTO temporal_rng2 VALUES ('[8,8]', mydaterange('2018-01-01', '2021-01-01'));
+INSERT INTO temporal_fk2_rng2rng VALUES ('[5,5]', mydaterange('2018-01-01', '2021-01-01'), '[8,8]');
+DELETE FROM temporal_rng2 FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]';
+SELECT * FROM temporal_fk2_rng2rng WHERE id = '[5,5]';
+
+DROP TABLE temporal_fk2_rng2rng;
+DROP TABLE temporal_rng2;
+DROP TYPE mydaterange;
 
 -- FK between partitioned tables
 
@@ -646,8 +739,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'),
@@ -661,8 +754,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
 
@@ -746,37 +839,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.25.1

From 4898052a22b16e5994a87ae0702c5d505767b7f1 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 9 Jan 2021 21:59:43 -0800
Subject: [PATCH v14 5/5] 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 +
 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              | 765 +++++++++++++++++-
 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            | 151 +++-
 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                       |  35 +
 src/include/catalog/dependency.h              |   1 +
 src/include/catalog/heap.h                    |   4 +
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/pg_index.h                |   2 +-
 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/syscache.h                  |   3 +
 .../test_ddl_deparse/test_ddl_deparse.c       |   6 +
 src/test/regress/expected/periods.out         | 173 ++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/periods.sql              | 117 +++
 48 files changed, 2318 insertions(+), 44 deletions(-)
 create mode 100644 src/backend/catalog/pg_period.c
 create mode 100644 src/include/catalog/pg_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 19c2e1c827..ba569d9790 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>
@@ -5712,6 +5717,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>perrngtype</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the range type associated with this period
+      </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 075ff32991..94a892ca29 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1263,6 +1263,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 d57a132340..8cc952a085 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -4162,6 +4162,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 2c4138e4e9..aee986efd5 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -60,6 +60,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>
@@ -571,6 +573,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 5b43c56b13..49c2df9944 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 fea8fc3c7a..c4a08b2ca6 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> ]
@@ -144,6 +152,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
@@ -804,6 +820,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/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 3e9994793d..500b80fd19 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 d1f5dcd8be..bd13f16b86 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2791,6 +2791,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:
@@ -2932,6 +2933,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 f8a136ba0a..232f77c96e 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 7148dd1787..ae7da0e821 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"
@@ -2039,6 +2040,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 8fc0fb4bcd..d8773ca485 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -1211,7 +1211,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 fa6609e577..c499cd2f5d 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 715201f5a2..f5f88d5fdd 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 0000000000..465b5b675e
--- /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 b33065d7bf..775090a6ec 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 ff8d003876..2ebe458648 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 26c4d59202..452af1d413 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 469a6c2ee9..1b9754e998 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 d4b00d1a82..19e6dfd37b 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -970,6 +970,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:
@@ -1028,6 +1029,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:
@@ -2069,6 +2071,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:
@@ -2153,6 +2156,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 7ff16e3276..3ddc99581a 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 b61853fd82..46724a88f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -43,6 +43,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"
@@ -149,13 +150,18 @@ static List *on_commits = NIL;
 #define AT_PASS_OLD_CONSTR		3	/* re-add existing constraints */
 /* We could support a RENAME COLUMN pass here, but not currently used */
 #define AT_PASS_ADD_COL			4	/* ADD COLUMN */
-#define AT_PASS_ADD_CONSTR		5	/* ADD constraints (initial examination) */
-#define AT_PASS_COL_ATTRS		6	/* set column attributes, eg NOT NULL */
-#define AT_PASS_ADD_INDEXCONSTR	7	/* ADD index-based constraints */
-#define AT_PASS_ADD_INDEX		8	/* ADD indexes */
-#define AT_PASS_ADD_OTHERCONSTR	9	/* ADD other constraints, defaults */
-#define AT_PASS_MISC			10	/* other stuff */
-#define AT_NUM_PASSES			11
+/*
+ * 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.
+ */
+#define AT_PASS_ADD_PERIOD		5	/* ADD PERIOD */
+#define AT_PASS_ADD_CONSTR		6	/* ADD constraints (initial examination) */
+#define AT_PASS_COL_ATTRS		7	/* set column attributes, eg NOT NULL */
+#define AT_PASS_ADD_INDEXCONSTR	8	/* ADD index-based constraints */
+#define AT_PASS_ADD_INDEX		9	/* ADD indexes */
+#define AT_PASS_ADD_OTHERCONSTR	10	/* ADD other constraints, defaults */
+#define AT_PASS_MISC			11	/* other stuff */
+#define AT_NUM_PASSES			12
 
 typedef struct AlteredTableInfo
 {
@@ -354,6 +360,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
 static List *MergeAttributes(List *schema, List *supers, char relpersistence,
 							 bool is_partition, List **supconstr,
 							 List **supnotnulls);
+static List *MergePeriods(char *relname, List *periods, List *tableElts, List *supers);
 static bool MergeCheckConstraint(List *constraints, char *name, Node *expr);
 static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel);
 static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
@@ -435,6 +442,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,
@@ -454,6 +463,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);
 static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName,
@@ -662,6 +677,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);

 /* ----------------------------------------------------------------
@@ -890,6 +909,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
@@ -1281,6 +1320,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);
 
 	/*
@@ -1292,6 +1346,264 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	return address;
 }
 
+/*
+ * 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
@@ -3281,6 +3593,168 @@ MergeAttributes(List *schema, 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
@@ -4326,12 +4800,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);
@@ -4340,7 +4814,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)
 
 	EventTriggerAlterTableRelid(relid);
 
-	ATController(NULL, rel, cmds, recurse, lockmode, NULL);
+	ATController(NULL, rel, cmds, recurse, lockmode, context);
 }
 
 /*
@@ -4433,6 +4907,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;
 
@@ -4748,6 +5224,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);
 			/* This command never recurses */
@@ -5145,6 +5629,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);
@@ -6289,6 +6781,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";
@@ -6312,6 +6806,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 */
@@ -7317,14 +7813,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.
@@ -7368,6 +7879,78 @@ 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.
+	 *
+	 * XXX: Does this hold for periods?
+	 */
+	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.
  */
@@ -8008,6 +8591,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
  *
@@ -13500,6 +14232,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				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 */
 				ereport(ERROR,
@@ -15561,7 +16302,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 9bd77546b9..be4637287f 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 62ccbf9d3c..a0c069fc5f 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 5a49ba6fa3..0eb6750922 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
@@ -2601,6 +2601,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
 				{
@@ -3712,8 +3730,10 @@ TableElement:
 			columnDef							{ $$ = $1; }
 			| TableLikeClause					{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
+			| TablePeriod						{ $$ = $1; }
 		;
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4065,6 +4085,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
@@ -7096,6 +7129,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 864ea9b0d5..fbbade5140 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 e195410c23..82d869db8a 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,
@@ -242,6 +246,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;
@@ -282,6 +287,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;
@@ -349,6 +358,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;
 
@@ -915,6 +925,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
@@ -1005,6 +1122,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	AttrNumber	parent_attno;
 	Relation	relation;
 	TupleDesc	tupleDesc;
+	Bitmapset  *periodatts;
 	AclResult	aclresult;
 	char	   *comment;
 	ParseCallbackState pcbstate;
@@ -1057,6 +1175,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.
@@ -1066,10 +1185,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.
 		 */
@@ -1676,6 +1803,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	Oid			keycoltype;
 	Datum		datum;
 	bool		isnull;
+	PeriodDef  *period;
 
 	if (constraintOid)
 		*constraintOid = InvalidOid;
@@ -1733,6 +1861,11 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	index->if_not_exists = false;
 	index->reset_default_tblspc = false;
 
+	/* Copy the period */
+	period = makeNode(PeriodDef);
+	period->oid = idxrec->indperiod;
+	index->period = period;
+
 	/*
 	 * We don't try to preserve the name of the source index; instead, just
 	 * let DefineIndex() choose a reasonable name.  (If we tried to preserve
@@ -3113,6 +3246,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.)
@@ -3570,6 +3707,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;
@@ -3632,6 +3770,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/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 391762f308..4c2965c151 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"
@@ -1021,6 +1022,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 ----------					 */
 
 /*
@@ -3532,6 +3595,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 8dbda0024f..44e636e5be 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 4d83381d84..728130c179 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3521,6 +3521,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 efe8d72b32..ea744bc3e3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6333,6 +6333,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;
@@ -6410,6 +6411,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, ");
@@ -6547,6 +6556,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");
@@ -6630,6 +6640,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);
@@ -8364,7 +8375,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
  */
@@ -8417,6 +8428,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)
@@ -8431,7 +8444,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 '{'? */
@@ -8967,15 +8981,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);
 
@@ -8997,6 +9032,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++)
@@ -9016,12 +9052,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);
 			}
@@ -9080,6 +9117,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);
@@ -10350,6 +10460,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;
@@ -15885,6 +15997,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.
 			 *
@@ -15893,7 +16031,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]);
 
@@ -16111,7 +16249,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]);
 
@@ -16413,7 +16551,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]);
 
@@ -18509,6 +18647,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 773a2de18b..199b25571b 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,
@@ -299,12 +300,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 */
@@ -354,6 +357,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 */
 
@@ -492,6 +496,16 @@ typedef struct _constraintInfo
 	bool		withoutoverlaps;	/* true if the last elem is WITHOUT OVERLAPS */
 } 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 523a19c155..55f2b68d0f 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -84,6 +84,7 @@ enum dbObjectTypePriorities
 	PRIO_CONSTRAINT,
 	PRIO_INDEX,
 	PRIO_INDEX_ATTACH,
+	PRIO_PERIOD,
 	PRIO_STATSEXT,
 	PRIO_RULE,
 	PRIO_TRIGGER,
@@ -118,6 +119,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 */
@@ -1438,6 +1440,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 4fbfc07460..e38b9a1c3a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1948,6 +1948,7 @@ 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);
+	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);
@@ -2370,6 +2371,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */
+		if (pset.sversion >= 150000)
+		{
+			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 ffd5e9dc82..8f03bf3de1 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 51f7b12aa3..1f646f3d3b 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 dcb3c5f766..19437291a4 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 32930411b6..acfbef9e27 100644
--- a/src/include/catalog/pg_index.h
+++ b/src/include/catalog/pg_index.h
@@ -44,11 +44,11 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO
 	bool		indisready;		/* is this index ready for inserts? */
 	bool		indislive;		/* is this index alive at all? */
 	bool		indisreplident; /* is this index the identity for replication? */
+	Oid			indperiod;		/* the period it contains, if any */
 
 	/* 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 0000000000..d2cffa07bc
--- /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 1247fa7c77..7ed014b71d 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 16b6126669..e918d9a633 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -38,7 +38,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);
 
@@ -103,5 +104,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 177df96c86..52de618d1f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2163,6 +2163,7 @@ typedef enum ObjectType
 	OBJECT_OPERATOR,
 	OBJECT_OPFAMILY,
 	OBJECT_PARAMETER_ACL,
+	OBJECT_PERIOD,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
 	OBJECT_PUBLICATION,
@@ -2250,6 +2251,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 (...) */
@@ -2516,11 +2519,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.
  * ----------------------
  */
 
@@ -2529,6 +2532,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 */
@@ -2543,6 +2547,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
  *
@@ -3252,6 +3280,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 2b40a0b5b1..feb6c26007 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 a117527d8a..f015b51c86 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -96,6 +96,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/syscache.h b/src/include/utils/syscache.h
index 67ea6e4945..33bab4061a 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 0302f79bb7..05f9780ecf 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -180,6 +180,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 0000000000..0f2b2d453d
--- /dev/null
+++ b/src/test/regress/expected/periods.out
@@ -0,0 +1,173 @@
+/* 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
+/* 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 99cf6f18d6..25167712c9 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 0000000000..516f1cef9d
--- /dev/null
+++ b/src/test/regress/sql/periods.sql
@@ -0,0 +1,117 @@
+/* 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;
+
+/* 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.25.1

---

From: jian he <[email-masked]>
Date: Mon, 18 Sep 2023 11:11:02 +0800

> PERIOD support. So this v14 patch is just some minor fixes & tweaks from

small issues so far I found, v14.

IndexInfo struct definition comment still has Temporal related
comment, should be removed.

catalog-pg-index.html, no indperiod doc entry, also in table pg_index,
column indperiod  is junk value now.
I think in UpdateIndexRelation, you need an add indperiod to build a
pg_index tuple, similar to what you did in CreateConstraintEntry.

seems to make the following query works, we need to bring btree_gist
related code to core?
CREATE TABLE temporal_fk_rng2rng22 (id int8, valid_at int4range,
unique (id, valid_at WITHOUT OVERLAPS));

/* ----------------
 * 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;

no idea what the above comment "cpp'' refers to. The sixth field in
FormData_pg_period: perrange, the comment conflict with catalogs.sgml
>> perrngtype oid (references pg_type.oid)
>> The OID of the range type associated with this period

create table pt (id integer, ds date, de date, period for p (ds, de));
SELECT table_name, column_name, column_default, is_nullable,
is_generated, generation_expression
FROM    information_schema.columns
WHERE table_name = 'pt' ORDER BY 1, 2;

the hidden generated column  (p)  is_nullable return NO. but ds, de
is_nullable both return YES. so column p is_nullable should return
YES?

---

From: jian he <[email-masked]>
Date: Mon, 18 Sep 2023 20:49:14 +0800

> ACTION and RESTRICT fixed (and reproductions added to the test suite).

another case:
BEGIN;
DROP TABLE IF EXISTS temporal_rng, temporal_fk_rng2rng;
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);
CREATE TABLE temporal_fk_rng2rng (
id int4range,
valid_at tsrange,
parent_id int4range
);
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]');
commit;

BEGIN;
ALTER TABLE temporal_fk_rng2rng
DROP CONSTRAINT IF EXISTS 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 NO ACTION
ON UPDATE NO ACTION;
ALTER TABLE temporal_fk_rng2rng ALTER CONSTRAINT
temporal_fk_rng2rng_fk  DEFERRABLE INITIALLY DEFERRED;

delete from temporal_rng; ---should not fail.
commit; ---fail in here.

-------------------------------
seems in ATExecAlterConstrRecurse change to

/*
* Update deferrability of RI_FKey_noaction_del,
* RI_FKey_noaction_upd, RI_FKey_check_ins and RI_FKey_check_upd
* triggers, but not others; see createForeignKeyActionTriggers
* and CreateFKCheckTrigger.
*/
if (tgform->tgfoid != F_RI_FKEY_NOACTION_DEL &&
tgform->tgfoid != F_TRI_FKEY_NOACTION_DEL &&
tgform->tgfoid != F_RI_FKEY_NOACTION_UPD &&
tgform->tgfoid != F_TRI_FKEY_NOACTION_UPD &&
tgform->tgfoid != F_RI_FKEY_CHECK_INS &&
tgform->tgfoid != F_TRI_FKEY_CHECK_INS &&
tgform->tgfoid != F_RI_FKEY_CHECK_UPD &&
tgform->tgfoid != F_TRI_FKEY_CHECK_UPD)
continue;

will work.

---

From: Paul Jungwirth <[email-masked]>
Date: Tue, 19 Sep 2023 19:50:10 -0700

> small issues so far I found, v14.

Thank you again for the review! v15 is attached.

> comment, should be removed.

Fixed.

> column indperiod  is junk value now.

You're right, it is just unneeded now that PERIODs are implemented by 
GENERATED columns. I've removed it.

> pg_index tuple, similar to what you did in CreateConstraintEntry.

It's gone now.

> CREATE TABLE temporal_fk_rng2rng22 (id int8, valid_at int4range, > unique (id, valid_at WITHOUT OVERLAPS));

It doesn't need to be brought into core, but you would need to say 
`CREATE EXTENSION btree_gist` first. Since the regression tests don't 
assume we've built contrib, we have to use a workaround there.

> no idea what the above comment "cpp'' refers to.

I believe cpp = C Pre-Processor. This comment is at the top of all the 
catalog/pg_*.h files. The next line is part of the same sentence (which 
took me a while to notice :-).

> The sixth field in

You're right, fixed! More cruft from the old PERIOD implementation.

> is_nullable both return YES. so column p is_nullable should return

The is_nullable behavior is correct I believe. In a range if the 
lower/upper value is NULL, it signifies the range has no lower/upper 
bound. So it's fine for ds or de to be NULL, but not the range itself (p).

Technically the SQL spec says that the PERIOD start & end columns should 
be NOT NULL, but that forces people to use ugly sentinel values like 
'3999-01-01'. It's a shame to make people do that when NULL works so 
well instead. Our time-related types do have Infinity and -Infinity 
which is not as ugly, but many other types do not. Plus those values 
interact badly with ranges. For example `select '(,)'::daterange - 
'(,Infinity)'::daterange` gives the infinitesimal result `[infinity,)`. 
I've heard at least one report of that make a mess in a user's database. 
If a user wants to make the start/end columns NOT NULL they can, so I 
prefer not to force them.

Continuing to your other email:

 > delete from temporal_rng; ---should not fail.

Great catch! This is fixed also.

Yours,

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

 24 files changed, 1100 insertions(+), 60 deletions(-)

+     <row>

 
 
      <para>
 
      <para>

      <para>
 
      <para>

 	pfree(ccbin);

 

 	/*
 
 	if (conkeyArray)

 
+	/*

 	/*
 
 		if (stmt->primary)
 
-						if (stmt->unique)

 	indexRelationId =
 
+/*

 
 		/*

 			/*

 

 

+		/*
 
 	/*
 
+/*
index 68f301484e..7aa5b62ab8 100644

 

 

 

 
 		if (j == 0)

 
 	/*

 	/*

 

 

 
 
+	/*
 
 	/*
 
 	/*

 
 
 # ----------

From 61330e25a607e4e5a92eeee5662330df8067842c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v15 2/5] 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.
---
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 src/backend/commands/tablecmds.c              | 764 +++++++++++-------
 src/backend/parser/gram.y                     |  37 +-
 src/backend/utils/adt/ri_triggers.c           | 341 +++++++-
 src/backend/utils/adt/ruleutils.c             |  19 +-
 src/include/catalog/pg_constraint.h           |   8 +-
 src/include/catalog/pg_proc.dat               |  24 +
 src/include/nodes/parsenodes.h                |   2 +
 src/include/parser/kwlist.h                   |   1 +
 .../regress/expected/without_overlaps.out     | 461 +++++++++++
 src/test/regress/sql/without_overlaps.sql     | 462 +++++++++++
 11 files changed, 1817 insertions(+), 344 deletions(-)

 
index ba2a8914e4..e01f64ae34 100644

 				/*

 

 	/*

 	/*

 
 
 			/*

 

+static void
@@ -11347,7 +11444,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
-				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);
@@ -11595,7 +11696,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11663,7 +11763,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;
@@ -11728,36 +11830,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11784,6 +11905,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11805,12 +11930,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11848,6 +11977,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -11957,7 +12099,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -11986,8 +12129,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;
 
 	/*
@@ -12056,6 +12201,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12075,12 +12221,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12138,37 +12290,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12198,37 +12381,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 -

index 7aa5b62ab8..99bbe6d97e 100644

 

 

 
 	/*
 
 	/*
 
 	/*

 
 
index 5e126df071..1749945e71 100644
@@ -357,3 +357,464 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
+-- 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.
index 3c3236618c..13ce7313c5 100644
@@ -260,3 +260,465 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+-- 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.

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

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

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e01f64ae34..f5a736fa96 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12172,6 +12172,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

 

 

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

 

 
+	/*

 

 
@@ -60,10 +62,17 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*

 
 	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:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 					/*
 
+/*
 
 Datum

 /*

 

 

 	/*
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
 
+/*
+ * get_typname
+ *
 
 /*
index cb714f4a19..fed3386429 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -422,6 +424,28 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -542,6 +566,9 @@ typedef struct ResultRelInfo

 
 
+/*
+ * ForPortionOfClause

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

 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 1749945e71..4b8abb081c 100644

@@ -556,6 +646,13 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -593,13 +690,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -621,6 +730,12 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
@@ -645,9 +760,19 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
 
 # ----------

index 13ce7313c5..960a632381 100644

 -- temporal UNIQUE:
 
 --
@@ -455,6 +501,11 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -490,13 +541,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -516,6 +577,10 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
@@ -540,9 +605,17 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-

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

---

index f5a736fa96..264ae21596 100644
@@ -12307,11 +12307,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12398,11 +12406,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*
index 4b8abb081c..f8a0c1d2c1 100644
 
@@ -785,7 +786,62 @@ ALTER TABLE temporal_fk_rng2rng
@@ -795,7 +851,67 @@ ALTER TABLE temporal_fk_rng2rng
@@ -807,7 +923,95 @@ ALTER TABLE temporal_fk_rng2rng
@@ -815,8 +1019,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -829,8 +1033,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -865,7 +1069,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -877,7 +1081,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
@@ -899,7 +1103,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -911,35 +1115,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
index 960a632381..7ea6af5184 100644
 
@@ -630,6 +631,28 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -640,6 +663,28 @@ ALTER TABLE temporal_fk_rng2rng
 
@@ -652,6 +697,54 @@ ALTER TABLE temporal_fk_rng2rng

@@ -661,8 +754,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -676,8 +769,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -761,37 +854,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 

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

 src/backend/parser/parse_utilcmd.c            | 145 +++-
 src/include/catalog/pg_index.h                |   1 -
 48 files changed, 2311 insertions(+), 44 deletions(-)

index 19c2e1c827..78d37877a1 100644
 
+     <row>
+       <structfield>perrange</structfield> <type>int2</type>
+       The number of the <literal>GENERATED</literal> column that implements the <literal>PERIOD</literal>.

 

 

+  <para>

+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+

+/*
index 7f7de91cc2..1c60701e36 100644

 /*
 
+		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 264ae21596..9c3b3fda87 100644

 
+	/*
 
+	/*
 
 	/*
 
+/*

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

 
 /*

 
+/*
 
+/*
@@ -13504,6 +14236,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15565,7 +16306,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*

+
 TypedTableElement:

+TablePeriod:

index e195410c23..03dcdb8cf9 100644

+			case T_PeriodDef:

+void

 	/*
 
+		/*
@@ -3113,6 +3240,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.)
@@ -3570,6 +3701,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;
@@ -3632,6 +3764,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*
index cc06f1c817..d294249fbd 100644
@@ -3500,6 +3500,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 

 

 
+		/* print periods */
 
index 32930411b6..a4e02ddca7 100644
@@ -48,7 +48,6 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO

 /*

 

 #endif							/* TABLECMDS_H */

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

 # ----------

---

From: jian he <[email-masked]>
Date: Mon, 25 Sep 2023 12:52:40 +0800

> Thank you again for the review! v15 is attached.

hi. some tiny issues.
IN src/backend/utils/adt/ri_triggers.c

else {
appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
pk_only, pkrelname);
}
should change to

else
{
appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
pk_only, pkrelname);
}

----
It would be better, we mention it somewhere:
by default, you can only have a primary key(range_type[...],
range_type WITHOUT OVERLAPS).

preceding without overlaps, all columns (in primary key) data types
only allowed range types.
-------------------------------
The WITHOUT OVERLAPS value must be a range type and is used to
constrain the record's applicability to just that interval (usually a
range of dates or timestamps).

"interval", I think "period" or "range" would be better. I am not sure
we need to mention " must be a range type, not a multi range type".
---------------------------------------------
I just `git apply`, then ran the test, and one test failed. Some minor
changes need to make the test pass.

---

From: Paul Jungwirth <[email-masked]>
Date: Mon, 25 Sep 2023 13:20:59 -0700

>> Thank you again for the review! v15 is attached.

Rebased v16 patches attached.

Fixed.

> only allowed range types.

I reworked those two paragraphs to incorporate those suggestions and 
hopefully clarify the idea bit further. (I'll revise these again once I 
support multiple WITHOUT OVERLAPS columns.)

> changes need to make the test pass.

I couldn't reproduce this. If you're still seeing a failure please let 
me know what you're seeing.

These patches also fix a problem I found with FKs when used with 
btree_gist. Privately I'm using the script below [1] to re-run all my 
tests with that extension and int+range columns. I'd like to add 
something similar to contrib/btree_gist. I'm open to advice how best to 
do that if anyone has any!

[1] #!/bin/bash
set -eu

# without_overlaps

cat ../src/test/regress/sql/without_overlaps.sql | \
   sed -E 's/int4range/integer/g' | \
   sed -E 's/valid_at integer/valid_at int4range/' | \
   sed -E 's/int8range/bigint/g' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),\1\]'"'"'/\1/g' | \
   cat > ./sql/without_overlaps.sql

cat ../src/test/regress/expected/without_overlaps.out | \
   sed -E 's/int4range/integer/g' | \
   sed -E 's/valid_at integer/valid_at int4range/' | \
   sed -E 's/incompatible types: integer and tsrange/incompatible types: 
int4range and tsrange/' | \
   sed -E 's/int8range/bigint/g' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),\1\]'"'"'/\1/g' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),-?[[:digit:]]+\)'"'"'/\1/g' | \
   sed -E 's/\[(-?[[:digit:]]+),\1\]/\1/g' | \
   sed -E 's/\[(-?[[:digit:]]+),-?[[:digit:]]+\)/\1/g' | \
   sed -E 'N;P;s/^ +id [^\n]+\n-+(\+.*)$/----\1/p;D' | \
   sed -E 
's/^----------\+-----------\+-----------\+----------\+---------$/----------+---------+-----------+----------+---------/' 
| \
   sed -E 
's/^----\+-------------------------\+--------\+-------$/----+-------------------------+-----+-------/' 
| \
   cat > ./expected/without_overlaps.out

# for_portion_of

cat ../src/test/regress/sql/for_portion_of.sql | \
   sed -E 's/int4range/integer/g' | \
   sed -E 's/valid_at integer/valid_at int4range/' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),\1\]'"'"'/\1/g' | \
   cat > ./sql/for_portion_of.sql

cat ../src/test/regress/expected/for_portion_of.out | \
   sed -E 's/int4range/integer/g' | \
   sed -E 's/valid_at integer/valid_at int4range/' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),\1\]'"'"'/\1/g' | \
   sed -E 's/'"'"'\[(-?[[:digit:]]+),-?[[:digit:]]+\)'"'"'/\1/g' | \
   sed -E 's/\[(-?[[:digit:]]+),\1\]/\1/g' | \
   sed -E 's/\[(-?[[:digit:]]+),-?[[:digit:]]+\)/\1/g' | \
   sed -E 'N;P;s/^ +id [^\n]+\n-+(\+.*)$/----\1/p;D' | \
   cat > ./expected/for_portion_of.out

Regards,

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

 doc/src/sgml/ref/create_table.sgml            |  45 ++-
 24 files changed, 1107 insertions(+), 60 deletions(-)

index e09adb45e4..312e00c90a 100644
 
+     <row>
index e04a0692c4..ad5bf21453 100644

 
 
      <para>
 
      <para>

      <para>
 
      <para>
@@ -1067,6 +1075,31 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       (e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
       index deletion.
      </para>
+
+     <para>
+      A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
+      is a <emphasis>temporal</emphasis> primary key.
+      The <literal>WITHOUT OVERLAPS</literal> column
+      must be a range type or a <literal>PERIOD</literal>
+      and is used to constrain the record's applicability
+      to just that range/period (usually a range of dates or timestamps,
+      although Postgres allows a range/period over any base type).
+     </para>
+
+     <para>
+      The non-<literal>WITHOUT OVERLAPS</literal> part(s) of the key 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).
+      These ordinary parts of the primary key do allow duplicates,
+      so long as the duplicates don't overlap in the
+      <literal>WITHOUT OVERLAPS</literal> column.
+      In effect a temporal <literal>PRIMARY KEY</literal> is enforced with an
+      <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
+      constraint. So for example <literal>PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)</literal>
+      behaves like <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH &amp;&amp;)</literal>.
+     </para>
     </listitem>
    </varlistentry>

 	pfree(ccbin);

 

 	/*
 
 	if (conkeyArray)

 
+	/*

 	/*
 
 		if (stmt->primary)
 
-						if (stmt->unique)

 	indexRelationId =
 
+/*

 
 		/*

 			/*

 

 

+		/*
 
 	/*
 
+/*

 

 

 

 
 		if (j == 0)

 
 	/*

 	/*

 

 

 
 
+	/*
 
 	/*
 
 	/*

 
 
 # ----------

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

 src/backend/commands/tablecmds.c              | 777 +++++++++++-------
 src/backend/utils/adt/ri_triggers.c           | 342 +++++++-
 11 files changed, 1831 insertions(+), 344 deletions(-)

index ad5bf21453..28cbbfb9aa 100644
@@ -1156,8 +1156,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1168,11 +1168,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1244,7 +1262,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1257,6 +1278,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1269,6 +1296,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
index ba2a8914e4..ed062d1e4c 100644

 				/*

 

 	/*

 	/*

 
 
 			/*

 
@@ -10843,13 +10721,245 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)

+static void
+		 * For the non-overlaps parts, we want either RTEqualStrategyNumber (without btree_gist)
+		 * or BTEqualStrategyNumber (with btree_gist). We'll try the latter first.
+		eqstrategy = for_overlaps ? RTOverlapStrategyNumber : BTEqualStrategyNumber;
+	/* Fall back to RTEqualStrategyNumber for temporal overlaps */
+	if (is_temporal && !for_overlaps && !OidIsValid(ppeqop))
+		eqstrategy = RTEqualStrategyNumber;
@@ -11347,7 +11457,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11595,7 +11709,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11663,7 +11776,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;
@@ -11728,36 +11843,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11784,6 +11918,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11805,12 +11943,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11848,6 +11990,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -11957,7 +12112,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -11986,8 +12142,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;
 
 	/*
@@ -12056,6 +12214,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12075,12 +12234,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12138,37 +12303,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12198,37 +12394,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

 

+optionalPeriodName:
index 6945d99b3d..aba0e62df6 100644
 
@@ -361,26 +365,58 @@ RI_FKey_check(TriggerData *trigdata)
 
 		/* ----------
 
@@ -390,6 +426,8 @@ RI_FKey_check(TriggerData *trigdata)
 
@@ -497,21 +535,49 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 
@@ -521,6 +587,8 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 
@@ -695,10 +763,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
 			quoteOneName(attname,
 						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
@@ -1213,6 +1287,126 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	}
 }
 
+/* ----------
+ * TRI_FKey_check_ins -
+ *
 
 /*
  * RI_FKey_pk_upd_check_required -
@@ -1241,7 +1435,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
 		return false;

 	/* Else we need to fire the trigger. */
@@ -1340,7 +1534,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
 		return true;

@@ -1488,15 +1682,17 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
 
@@ -2137,6 +2333,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
@@ -2171,6 +2368,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2791,9 +2989,12 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)

 /*
- * ri_KeysEqual -
+ * ri_KeysStable -
  *
@@ -2801,7 +3002,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
@@ -2834,35 +3035,86 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 
 		if (rel_is_pk)
 		{
-			/*

 	return true;
 }
 
+/*
 
 /*
  * ri_AttributesEqual -
@@ -3021,10 +3273,16 @@ RI_FKey_trigger_type(Oid tgfoid)

 

 

 

 	/*
 
 	/*
 
 	/*

 

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

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

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ed062d1e4c..09a69e762b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12185,6 +12185,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

 

 

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

 

 
+	/*

 

 
@@ -60,10 +62,17 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*

 
 	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:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 					/*
 
+/*
 
 Datum
index aba0e62df6..8552f434d5 100644

 /*
@@ -444,6 +450,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, 0,
 					pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE,
 					SPI_OK_SELECT);
 
@@ -601,6 +608,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 	result = ri_PerformCheck(riinfo, &qkey, qplan,
 							 fk_rel, pk_rel,
 							 oldslot, NULL,
+							 -1, 0,
 							 true,	/* treat like update */
 							 SPI_OK_SELECT);
 
@@ -700,6 +708,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = 0;
 
@@ -796,9 +806,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -905,6 +922,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_DELETE);
 
@@ -1026,6 +1044,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, newslot,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -1258,6 +1277,7 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2520,6 +2540,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2575,6 +2596,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
 	 * In READ COMMITTED mode, we just need to use an up-to-date regular
@@ -3288,3 +3315,50 @@ RI_FKey_trigger_type(Oid tgfoid)
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
 
+/*
+ * get_typname
+ *
 
 /*

+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

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

index 0000000000..ae986a116b
+('[1,1]', '[2018-01-02,2020-01-01)', 'one');
+('[1,1]', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', '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')
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[4,4]';
+WHERE id = '[4,4]';
+WHERE id = '[4,4]';
+SET id = '[6,6]'
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[6,6]';
+WHERE id = '[3,3]'
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 # ----------
index 0000000000..f1a2140701
+('[1,1]', '[2018-01-02,2020-01-01)', 'one');
+('[1,1]', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', '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')
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[4,4]';
+WHERE id = '[4,4]';
+WHERE id = '[4,4]';
+SET id = '[6,6]'
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[3,3]';
+WHERE id = '[6,6]';
+WHERE id = '[3,3]'

 
 
 -- temporal UNIQUE:

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

---

index 09a69e762b..b9dd540492 100644
@@ -12320,11 +12320,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12411,11 +12419,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 8552f434d5..d002791855 100644
 
@@ -1391,6 +1398,127 @@ TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
 
+/*
@@ -1428,6 +1556,415 @@ TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
 
+/*
@@ -2552,8 +3089,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -2588,8 +3125,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -3300,8 +3839,14 @@ RI_FKey_trigger_type(Oid tgfoid)

 

 

 

 

 

 

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

 src/bin/psql/describe.c                       |  36 +
 48 files changed, 2312 insertions(+), 44 deletions(-)

index 312e00c90a..13c11b885a 100644
 
+     <row>

 

index 28cbbfb9aa..6d2a77e477 100644

 
+  <para>

+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+

+/*
index 10b34c3c5b..1104331289 100644
@@ -1210,7 +1210,28 @@ GRANT SELECT ON parameters TO PUBLIC;
  * PERIODS view
  */

 /*
 
+		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 bd812e42d9..20d687b4fd 100644
@@ -976,6 +976,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
@@ -1034,6 +1035,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
@@ -2075,6 +2077,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2159,6 +2162,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index b9dd540492..5ae0f1136a 100644

 
+	/*
 
+	/*
 
 	/*
 
+/*

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

 
 /*

 
+/*
 
+/*
@@ -13517,6 +14249,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15578,7 +16319,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*

+
 TypedTableElement:

+TablePeriod:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 

 
+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 abfea15c09..8324f502dc 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,
@@ -119,6 +120,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 */
@@ -1368,6 +1370,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 4fbfc07460..bfb2909b94 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1948,6 +1948,8 @@ describeOneTableDetails(const char *schemaname,
 
+	if (pset.sversion >= 170000)
 
 	res = PSQLexec(buf.data);
@@ -2370,6 +2372,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */

 
 /*

 

 #endif							/* TABLECMDS_H */

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

 # ----------

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 25 Sep 2023 22:00:30 +0100

> Rebased v16 patches attached.

Looking through the tests in v16-0001:

I think this error is confusing.  The SQL standard requires at least one 
non-period column in a PK.  I don't know why that is or why we should 
implement it.  But if we want to implement it, maybe we should enforce 
that in parse analysis rather than directly in the parser, to be able to 
produce a more friendly error message.

+-- 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:  range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist

I think here we should just produce a "column doesn't exist" error 
message, the same as if the "id" column was invalid.  We don't need to 
get into the details of what kind of column it should be.  That is done 
in the next test

Also, in any case it would be nice to have a location pointer here (for 
both cases).

+       CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT 
OVERLAPS)
+);

I'm confused why you are using int4range here (and in further tests) for 
the scalar (non-range) part of the primary key.  Wouldn't a plaint int4 
serve here?

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

Shouldn't this somehow show the operator classes for the columns?  We 
are using different operator classes for the id and valid_at columns, 
aren't we?

+DETAIL:  Cannot create a primary key or unique constraint using such an 
index.

Could you also add a test where the index is unique and the whole thing 
does work?

Apart from the tests, how about renaming the column 
pg_constraint.contemporal to something like to conwithoutoverlaps?

---

From: Paul Jungwirth <[email-masked]>
Date: Tue, 10 Oct 2023 21:22:35 -0700

Hi Peter et al,

On 9/1/23 12:56, Paul Jungwirth wrote:
> functionality.

I've been working on implementing this, but I've come to think it is the 
wrong way to go.

If we support this in primary key and unique constraints, then we must 
also support it for foreign keys and UPDATE/DELETE FOR PORTION OF. But 
implementing that logic is pretty tricky. For example take a foreign key 
on (id, PERIOD valid_at, PERIOD asserted_at). We need to ensure the 
referenced two-dimensional time space `contains` the referencing 
two-dimensional space. You can visualize a rectangle in two-dimensional 
space for each referencing record (which we validate one at a time). The 
referenced records must be aggregated and so form a polygon (of all 
right angles). For example the referencing record may be (1, [0,2), 
[0,2)) with referenced records of (1, [0,2), [0,1)) and (1, [0,1), 
[1,2)). (I'm using intranges since they're easier to read, but you could 
imagine these as dateranges like [2000-01-01,2002-01-01).) Now the 
range_agg of their valid_ats is [0,2) and of their asserted_ats is 
[0,2). But the referenced 2d space still doesn't contain the referencing 
space. It's got one corner missing. This is a well-known problem among 
game developers. We're lucky not to have arbitrary polygons, but it's 
still a tough issue.

Besides `contains` we also need to compute `overlaps` and `intersects` 
to support these temporal features. Implementing that for 2d, 3d, etc 
looks very complicated, for something that is far outside the normal use 
case and also not part of the standard. It will cost a little 
performance for the normal 1d use case too.

I think a better approach (which I want to attempt as an add-on patch, 
not in this main series) is to support not just range types, but any 
type with the necessary operators. Then you could have an mdrange 
(multi-dimensional range) or potentially even an arbitrary n-dimensional 
polygon. (PostGIS has something like this, but its `contains` operator 
compares (non-concave) *bounding boxes*, so it would not work for the 
example above. Still the similarity between temporal and spatial data is 
striking. I'm going to see if I can get some input from PostGIS folks 
about how useful any of this is to them.) This approach would also let 
us use multiranges: not for multiple dimensions, but for non-contiguous 
time spans stored in a single row. This puts the complexity in the types 
themselves (which seems more appropriate) and is ultimately more 
flexible (supporting not just mdrange but also multirange, and other 
things too).

This approach also means that instead of storing a mask/list of which 
columns use WITHOUT OVERLAPS, I can just store one attnum. Again, this 
saves the common use-case from paying a performance penalty to support a 
much rarer one.

I've still got my multi-WITHOUT OVERLAPS work, but I'm going to switch 
gears to what I've described here. Please let me know if you disagree!

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Tue, 10 Oct 2023 21:47:01 -0700

> produce a more friendly error message.

Okay.

(I think the reason the standard requires one non-period column is to 
identify the "entity". If philosophically the row is an Aristotelian 
proposition about that thing, the period qualifies it as true just 
during some time span. So the scalar part is doing the work that a PK 
conventionally does, and the period part does something else. Perhaps a 
PK/UNIQUE constraint with no scalar part would still be useful, but not 
very often I think, and I'm not sure it makes sense to call it PK/UNIQUE.)

> get into the details of what kind of column it should be.  That is done 

I'll change it. The reason for the different wording is that it might 
not be a column at all. It might be a PERIOD. So what about just "column 
or PERIOD doesn't exist"? (Your suggestion is fine too though.)

> Also, in any case it would be nice to have a location pointer here (for 

Agreed.

> the scalar (non-range) part of the primary key.  Wouldn't a plaint int4 

A plain int4 would be better, and it would match the normal use-case, 
but you must have btree_gist to create an index like that, and the 
regress tests can't assume we have that. Here is the part from 
sql/rangetypes.sql I'm referring to:

--
-- Btree_gist is not included by default, so to test exclusion
-- constraints with range types, use singleton int ranges for the "="
-- portion of the constraint.
--

create table test_range_excl(
   room int4range,
   speaker int4range,
   during tsrange,
   exclude using gist (room with =, during with &&),
   exclude using gist (speaker with =, during with &&)
);

> conname = 'temporal_rng_pk';

We only print the operator classes if they are not the default, so they 
don't appear here.

I do suspect something more is desirable though. For exclusion 
constraints we replace everything before the columns with just "EXCLUDE 
USING gist". I could embed WITHOUT OVERLAPS but it's not valid syntax in 
CREATE INDEX. Let me know if you have any ideas.

> Could you also add a test where the index is unique and the whole thing 

No problem!

> pg_constraint.contemporal to something like to conwithoutoverlaps?

Is that too verbose? I've got some code already changing it to 
conoverlaps but I'm probably happier with conwithoutoverlaps, assuming 
no one else minds it.

Yours,

---

From: Vik Fearing <[email-masked]>
Date: Fri, 13 Oct 2023 02:48:28 +0200

> CREATE INDEX. Let me know if you have any ideas.

Why not?  The standard does not mention indexes (although some 
discussions last week might change that) so we can change the syntax for 
it as we wish.  Doing so would also allow us to use ALTER TABLE ... 
USING INDEX for such things.
-- 
Vik Fearing

---

From: jian he <[email-masked]>
Date: Mon, 16 Oct 2023 12:33:10 +0800

> Rebased v16 patches attached.

Can you rebase it?
changes in
https://git.postgresql.org/cgit/postgresql.git/log/src/backend/executor/nodeModifyTable.c
https://git.postgresql.org/cgit/postgresql.git/log/src/backend/commands/tablecmds.c
make it  no longer applicable.

I try to manually edit the patch to make it applicable.
but failed at tablecmds.c

---

From: jian he <[email-masked]>
Date: Fri, 20 Oct 2023 20:45:05 +0800

Hi.
based on v16.

/* 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->range_name_location)));
attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
// TODO: check attr->attisdropped (?),
// and figure out concurrency issues with that in general.
// It should work the same as updating any other column.

I don't think we need to check attr->attisdropped here.
because the above function attnameAttNum already does the job.
--------------------------------------------
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);
return *typnamespace;

"return *typnamespace;" should be "return true"?
Maybe name it  to get_typname_and_typnamespace?
-----------------------------------------------------------------------
if (!get_typname_and_namespace(attr->atttypid, &range_type_name,
&range_type_namespace))
elog(ERROR, "missing range type %d", attr->atttypid);

you can just `elog(ERROR, "missing range type %s", range_type_name);` ?
Also, this should be placed just below if (!type_is_range(attr->atttypid))?
-----------------------------------------------------------------------
src/backend/catalog/objectaddress.c

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));
}

periods are always associated with the table, is the above else branch correct?
-----------------------------------------------------------------------
File: src/backend/commands/tablecmds.c
7899: /*
7900: * this test is deliberately not attisdropped-aware, since if one tries to
7901: * add a column matching a dropped column name, it's gonna fail anyway.
7902: *
7903: * XXX: Does this hold for periods?
7904: */
7905: attTuple = SearchSysCache2(ATTNAME,
7906:    ObjectIdGetDatum(RelationGetRelid(rel)),
7907:    PointerGetDatum(pername));

XXX: Does this hold for periods?
Yes. we can add the following 2 sql for code coverage.
alter table pt add period for tableoid (ds, de);
alter table pt add period for "........pg.dropped.4........" (ds, de);

---

From: jian he <[email-masked]>
Date: Mon, 23 Oct 2023 08:00:00 +0800

hi. also based on v16.
-----------------tests.
drop table if exists for_portion_of_test1;
CREATE unlogged TABLE for_portion_of_test1 (id int4range, valid_at
tsrange,name text );
INSERT INTO for_portion_of_test1 VALUES  ('[1,1]', NULL,
'[1,1]_NULL'),('[1,1]', '(,)', '()_[1,]')
,('[1,1]', 'empty', '[1,1]_empty'),(NULL,NULL, NULL), (nuLL,
'(2018-01-01,2019-01-01)','misc');
--1
UPDATE for_portion_of_test1 FOR PORTION OF valid_at FROM NULL TO NULL
SET name = 'for_portition_NULLtoNULL';
select * from for_portion_of_test1;
--2
UPDATE for_portion_of_test1 FOR PORTION OF valid_at FROM null TO
UNBOUNDED SET name = 'NULL_TO_UNBOUNDED';
select * from for_portion_of_test1;
--3
UPDATE for_portion_of_test1 FOR PORTION OF valid_at FROM UNBOUNDED TO
null SET name = 'UNBOUNDED__TO_NULL';
select * from for_portion_of_test1;
--4
UPDATE for_portion_of_test1 FOR PORTION OF valid_at FROM UNBOUNDED TO
UNBOUNDED SET name = 'UNBOUNDED__TO_UNBOUNDED';
select * from for_portion_of_test1;
------------------------
File: /src/backend/executor/nodeModifyTable.c
1277: oldRange = slot_getattr(oldtupleSlot,
forPortionOf->rangeVar->varattno, &isNull);
1278:
1279: if (isNull)
1280: elog(ERROR, "found a NULL range in a temporal table");
1281: oldRangeType = DatumGetRangeTypeP(oldRange);

I wonder when this isNull will be invoked. the above tests won't
invoke the error.
also the above test, NULL seems equivalent to unbounded. FOR PORTION
OF "from" and "to" both bound should not be null?

which means the following code does not work as intended? I also
cannot find a way to invoke the following elog error branch.
File:src/backend/executor/nodeModifyTable.c
4458: exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
4459: targetRange = ExecEvalExpr(exprState, econtext, &isNull);
4460: if (isNull)
4461: elog(ERROR, "Got a NULL FOR PORTION OF target range");

---------------------------
i also made some changes in the function range_leftover_internal,
ExecForPortionOfLeftovers.
please see the attached patch.
From 5cdd99f9a63d381985541b0df539c2bb92ef7276 Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Sat, 21 Oct 2023 17:18:50 +0800
Subject: [PATCH v1 1/1] refactor function range_leftover_internal

renaming variable.
better comment.

---
 src/backend/utils/adt/rangetypes.c | 55 +++++++++++++++++-------------
 src/include/utils/rangetypes.h     |  5 ++-
 2 files changed, 33 insertions(+), 27 deletions(-)

diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index d4e17323..a5a34450 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -1207,47 +1207,54 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
 }
 
 /*
- * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1
- * after subtracting r2, or if nothing is left then to the empty range.
- * output1 will always be "before" r2 and output2 "after".
+ * Sets leftoverRange and rightoverRange to the remaining parts of oldRange
+ * after subtracting targetRange, or if nothing is left then to the empty range.
+ * leftoverRange will always be "before" targetRange and rightoverRange "after" targetRange.
  */
 void
-range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
-						const RangeType *r2, RangeType **output1, RangeType **output2)
+range_leftover_internal(TypeCacheEntry *typcache, const RangeType *oldRange,
+						const RangeType *targetRange, RangeType **leftoverRange, RangeType **rightoverRange)
 {
-	RangeBound	lower1,
-				lower2;
-	RangeBound	upper1,
-				upper2;
-	bool		empty1,
-				empty2;
+	RangeBound	lower_oldRange,
+				lower_targetRange;
+	RangeBound	upper_oldRange,
+				upper_targetRange;
+	bool		oldRange_is_empty,
+				targetRange_is_empty;
 
-	range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
-	range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+	range_deserialize(typcache, oldRange, &lower_oldRange, &upper_oldRange, &oldRange_is_empty);
+	range_deserialize(typcache, targetRange, &lower_targetRange, &upper_targetRange, &targetRange_is_empty);
 
-	if (range_cmp_bounds(typcache, &lower1, &lower2) < 0)
+	/*
+	* FOR PORTION OF. upper_targetRange, if oldRange do not overlaps with targetRangeType.
+	* So these two range must overlaps, that means both range should not be empty.
+	*
+	*/
+	Assert(!oldRange_is_empty);
+	Assert(!targetRange_is_empty);
+
+	if (range_cmp_bounds(typcache, &lower_oldRange, &lower_targetRange) < 0)
 	{
-		lower2.inclusive = !lower2.inclusive;
-		lower2.lower = false;
-		*output1 = make_range(typcache, &lower1, &lower2, false, NULL);
+		lower_targetRange.inclusive = !lower_targetRange.inclusive;
+		lower_targetRange.lower = false;
+		*leftoverRange = make_range(typcache, &lower_oldRange, &lower_targetRange, false, NULL);
 	}
 	else
 	{
-		*output1 = make_empty_range(typcache);
+		*leftoverRange = make_empty_range(typcache);
 	}
 
-	if (range_cmp_bounds(typcache, &upper1, &upper2) > 0)
+	if (range_cmp_bounds(typcache, &upper_oldRange, &upper_targetRange) > 0)
 	{
-		upper2.inclusive = !upper2.inclusive;
-		upper2.lower = true;
-		*output2 = make_range(typcache, &upper2, &upper1, false, NULL);
+		upper_targetRange.inclusive = !upper_targetRange.inclusive;
+		upper_targetRange.lower = true;
+		*rightoverRange = make_range(typcache, &upper_targetRange, &upper_oldRange, false, NULL);
 	}
 	else
 	{
-		*output2 = make_empty_range(typcache);
+		*rightoverRange = make_empty_range(typcache);
 	}
 }
-
 /* range -> range aggregate functions */
 
 Datum
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 48ee2deb..f67ae402 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -164,8 +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_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
-									const RangeType *r2, RangeType **output1,
-									RangeType **output2);
+extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *oldRange,
+						const RangeType *targetRange, RangeType **leftoverRange, RangeType **rightoverRange);
 
2.34.1

From 1dff6266fabca760681512fc69ec6b7ccce60896 Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Sun, 22 Oct 2023 12:48:05 +0800
Subject: [PATCH v1 1/1] refactor ExecForPortionOfLeftovers.

FOR PORTION OF UPDATE/DELETE, upper and lower leftover range tuple
will have same the partition key as pre-update/delete tuple.
So refactor some comments.

ExecForPortionOfLeftovers is excuted inside ExecUpdateEpilogue or
ExecDeleteEpilogue, it means at least one part of leftover range
is not empty. in leftoverRangeType1, leftoverRangeType2 at least one is
not empty. So instead of call ExecFetchSlotHeapTuple twice, call it
once.
based on other code, add bool shouldFree.
---
 src/backend/executor/nodeModifyTable.c | 33 +++++++++++++++-----------
 1 file changed, 19 insertions(+), 14 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 92d6611f..063efa79 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1232,7 +1232,7 @@ static void set_leftover_tuple_bounds(TupleTableSlot *leftoverTuple,
 /* ----------------------------------------------------------------
  *		ExecForPortionOfLeftovers
  *
- *		Insert tuples for the untouched timestamp of a row in a FOR
+ *		Insert tuples for the untouched range of a row in a FOR
  *		PORTION OF UPDATE/DELETE
  * ----------------------------------------------------------------
  */
@@ -1257,6 +1257,8 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
 	TupleTableSlot *leftoverTuple1 = fpoState->fp_Leftover1;
 	TupleTableSlot *leftoverTuple2 = fpoState->fp_Leftover2;
+	HeapTuple oldtuple;
+	bool	shouldFree;
 
 	/*
 	 * Get the range of the old pre-UPDATE/DELETE tuple,
@@ -1303,18 +1305,19 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	range_leftover_internal(typcache, oldRangeType, targetRangeType, &leftoverRangeType1,
 			&leftoverRangeType2);
 
+	/* fetch the existing, old pre-UPDATE/DELETE tuple */
+	oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, &shouldFree);
+
 	/*
 	 * Insert a copy of the tuple with the lower leftover range.
-	 * Even if the table is partitioned,
-	 * our insert won't extend past the current row, so we don't need to re-route.
-	 * TODO: Really? What if you update the partition key?
+	 * Even if the table is partitioned and we did UPDATE/DELETE the partition key.
+	 * the partition key of the lower leftover range tuple
+	 * will be the same as the partition key of the original tuple.
+	 *
 	 */
-
 	if (!RangeIsEmpty(leftoverRangeType1))
 	{
-		// TODO: anything we need to clear here?
-		// Are we in the row context?
-		HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
+		// TODO: Are we in the row context?
 		ExecForceStoreHeapTuple(oldtuple, leftoverTuple1, false);
 
 		set_leftover_tuple_bounds(leftoverTuple1, forPortionOf, typcache, leftoverRangeType1);
@@ -1325,15 +1328,14 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	}
 
 	/*
-	 * Insert a copy of the tuple with the upper leftover range
-	 * Even if the table is partitioned,
-	 * our insert won't extend past the current row, so we don't need to re-route.
-	 * TODO: Really? What if you update the partition key?
+	 * Insert a copy of the tuple with the upper leftover range.
+	 * Even if the table is partitioned and we did UPDATE/DELETE the partion key.
+	 * the partition key of the (upper leftover range) tuple
+	 * will be the same as the partition key of the original tuple.
+	 *
 	 */
-
 	if (!RangeIsEmpty(leftoverRangeType2))
 	{
-		HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, NULL);
 		ExecForceStoreHeapTuple(oldtuple, leftoverTuple2, false);
 
 		set_leftover_tuple_bounds(leftoverTuple2, forPortionOf, typcache, leftoverRangeType2);
@@ -1342,6 +1344,9 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 		// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
 		ExecInsert(context, resultRelInfo, leftoverTuple2, node->canSetTag, NULL, NULL);
 	}
+
+	if (shouldFree)
+		heap_freetuple(oldtuple);
 }
 
2.34.1

---

From: jian he <[email-masked]>
Date: Wed, 25 Oct 2023 14:14:34 +0800

> > Also, in any case it would be nice to have a location pointer here (for

I refactored findNeworOldColumn to better handle error reports.
please check the attached.
From 81a5eab2596c0e322dee8165ad73343328f496ed Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Wed, 25 Oct 2023 13:50:38 +0800
Subject: [PATCH v1 1/1] refactor findNewOrOldColumn.

refator findNeworOldColumn to validate WITHOUT OVERLAP constraint
associated key column exists and it's a range data type.

create/alter table add without overlap constraint:
if the key column not exists,
it will report the same as no "without overlap" normal case.

create table add without overlap constraint:
if overlap constraint associated column's data type is not range,
it will report it's not a range error, also print out the
parser position.

alter table add without overlap constraint:
if overlap constraint associated column's data type is not range,
it will report not a range data type error.
it will not print out parser position (I don't know how to do that).
---
 src/backend/parser/parse_utilcmd.c            | 81 ++++++++++---------
 .../regress/expected/without_overlaps.out     |  8 +-
 2 files changed, 47 insertions(+), 42 deletions(-)

diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e195410c..f44de591 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,8 +124,7 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
 static void transformIndexConstraints(CreateStmtContext *cxt);
 static IndexStmt *transformIndexConstraint(Constraint *constraint,
 										   CreateStmtContext *cxt);
-static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname,
-							   Oid *typid);
+static void validate_without_overlaps(CreateStmtContext *cxt, char *colname);
 static void transformExtendedStatistics(CreateStmtContext *cxt);
 static void transformFKConstraints(CreateStmtContext *cxt,
 								   bool skipValidation,
@@ -2698,34 +2697,19 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 		 * Anything in without_overlaps should be included,
 		 * but with the overlaps operator (&&) instead of equality.
 		 */
-		if (constraint->without_overlaps != NULL) {
+		if (constraint->without_overlaps != NULL)
+		{
 			char *without_overlaps_str = strVal(constraint->without_overlaps);
 			IndexElem *iparam = makeNode(IndexElem);
-			char   *typname;
-			Oid		typid;
 
 			/*
 			 * Iterate through the table's columns
 			 * (like just a little bit above).
 			 * If we find one whose name is the same as without_overlaps,
 			 * validate that it's a range type.
-			 *
-			 * Otherwise report an error.
+			 * if it cannot found, raise an error.
 			 */
-
-			if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid))
-			{
-				if (!type_is_range(typid))
-					ereport(ERROR,
-							(errcode(ERRCODE_DATATYPE_MISMATCH),
-							 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type",
-									without_overlaps_str)));
-			}
-			else
-				ereport(ERROR,
-						(errcode(ERRCODE_UNDEFINED_COLUMN),
-						 errmsg("range or PERIOD \"%s\" in WITHOUT OVERLAPS does not exist",
-								without_overlaps_str)));
+			validate_without_overlaps(cxt, without_overlaps_str);
 
 			iparam->name = pstrdup(without_overlaps_str);
 			iparam->expr = NULL;
@@ -2744,6 +2728,9 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			{
 				/*
 				 * Force the column to NOT NULL since it is part of the primary key.
+				 * prior loop foreach(lc, constraint->keys) does not loop over
+				 * without_overlap keys. So we need manually add here.
+				 *
 				 */
 				AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
 
@@ -2872,31 +2859,37 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 }
 
 /*
- * Tries to find a column by name among the existing ones (if it's an ALTER TABLE)
- * and the new ones. Sets typname and typid if one is found. Returns false if we
- * couldn't find a match.
+ * Tries to find and validate the typid associated
+ * with a without_overlaps column name, even in ALTER TABLE case.
+ * After check, without_overlaps column should exist, and it's a range type.
+ *
  */
-static bool
-findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid)
+static void
+validate_without_overlaps(CreateStmtContext *cxt, char *colname)
 {
 	/* Check the new columns first in case their type is changing. */
-
 	ColumnDef  *column = NULL;
 	ListCell   *columns;
+	Oid			typid;
+	bool		found	= false;
 
 	foreach(columns, cxt->columns)
 	{
 		column = lfirst_node(ColumnDef, columns);
 		if (strcmp(column->colname, colname) == 0)
 		{
-			*typid = typenameTypeId(NULL, column->typeName);
-			*typname = TypeNameToString(column->typeName);
-			return true;
+			typid = typenameTypeId(NULL, column->typeName);
+			if (!type_is_range(typid))
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type",colname),
+						 parser_errposition(cxt->pstate, column->location)));
+			found = true;
+			break;
 		}
 	}
 
 	/* Look up columns on existing table. */
-
 	if (cxt->isalter)
 	{
 		Relation rel = cxt->rel;
@@ -2905,22 +2898,30 @@ findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *t
 			Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
 			const char *attname;
 
-			if (attr->attisdropped)
-				continue;
-
 			attname = NameStr(attr->attname);
 			if (strcmp(attname, colname) == 0)
 			{
-				Type type = typeidType(attr->atttypid);
-				*typid = attr->atttypid;
-				*typname = pstrdup(typeTypeName(type));
-				ReleaseSysCache(type);
-				return true;
+				typid = attr->atttypid;
+
+				if (attr->attisdropped)
+				{
+					found = false;
+					break;
+				}
+				if (!type_is_range(typid))
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type", colname)));
+				found = true;
+				break;
 			}
 		}
 	}
 
-	return false;
+	if (!found)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column \"%s\" named in key does not exist",colname)));
 }
 
 /*
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
index 4b8abb08..a916e56a 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -19,7 +19,7 @@ CREATE TABLE temporal_rng (
 	id INTEGER,
 	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 );
-ERROR:  range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+ERROR:  column "valid_at" named in key does not exist
 -- PK with a non-range column:
 CREATE TABLE temporal_rng (
 	id INTEGER,
@@ -27,6 +27,8 @@ CREATE TABLE temporal_rng (
 	CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 );
 ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range type
+LINE 3:  valid_at TEXT,
+         ^
 -- PK with one column plus a range:
 CREATE TABLE temporal_rng (
 	-- Since we can't depend on having btree_gist here,
@@ -110,7 +112,7 @@ CREATE TABLE temporal_rng2 (
 	id INTEGER,
 	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
 );
-ERROR:  range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+ERROR:  column "valid_at" named in key does not exist
 -- UNIQUE with a non-range column:
 CREATE TABLE temporal_rng2 (
 	id INTEGER,
@@ -118,6 +120,8 @@ CREATE TABLE temporal_rng2 (
 	CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
 );
 ERROR:  column "valid_at" in WITHOUT OVERLAPS is not a range type
+LINE 3:  valid_at TEXT,
+         ^
 -- UNIQUE with one column plus a range:
 CREATE TABLE temporal_rng2 (
 	-- Since we can't depend on having btree_gist here,
-- 
2.34.1

---

From: jian he <[email-masked]>
Date: Sat, 28 Oct 2023 16:25:57 +0800

V16 patch  doc/src/sgml/html/sql-createtable.html doc SET NULL description:
`
SET NULL [ ( column_name [, ... ] ) ]
Set all of the referencing columns, or a specified subset of the
referencing columns, to null. A subset of columns can only be
specified for ON DELETE actions.
In a temporal foreign key, the change will use FOR PORTION OF
semantics to constrain the effect to the bounds of the referenced row.
`

I think it means, if the foreign key has PERIOD column[s], then the
PERIOD column[s] will not be set to NULL in {ON DELETE|ON UPDATE}. We
can also use FOR PORTION OF semantics to constrain the effect to the
bounds of the referenced row.
see below demo:

BEGIN;
drop table if exists temporal_rng CASCADE;
drop table if exists temporal_fk_rng2rng CASCADE;
CREATE unlogged TABLE temporal_rng (id int4range,valid_at tsrange);
ALTER TABLE temporal_rng ADD CONSTRAINT temporal_rng_pk PRIMARY KEY
(id, valid_at WITHOUT OVERLAPS);
CREATE unlogged TABLE temporal_fk_rng2rng (id int4range,valid_at
tsrange,parent_id int4range,
CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng (id, PERIOD valid_at) on update set null ON
DELETE SET NULL);

INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01',
'2021-01-01'));
INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01',
'2021-01-01'), '[11,11]');
DELETE FROM temporal_rng WHERE id = '[11,11]';
table temporal_fk_rng2rng;
commit;
-----------------------------------------------------
also
"REFERENCES temporal_rng (id, PERIOD valid_at) ON UPDATE SET NULL ON
DELETE SET NULL)"
is the same as
"REFERENCES temporal_rng (id, PERIOD valid_at) ON UPDATE SET NULL ON
DELETE SET NULL (parent_id)"
in the current implementation.
we might need to change the pg_constraint column "confdelsetcols" description.
-------
the above also applies to SET DEFAULT.

--------------------------------------------------------------------------------------------------------------------------
can you add the following for the sake of code coverage. I think
src/test/regress/sql/without_overlaps.sql can be simplified.

--- common template for test foreign key constraint.
CREATE OR REPLACE PROCEDURE overlap_template()
LANGUAGE SQL
AS $$
DROP TABLE IF EXISTS temporal_rng CASCADE;
DROP TABLE IF EXISTS temporal_fk_rng2rng CASCADE;
CREATE UNLOGGED TABLE temporal_rng (id int4range,valid_at tsrange);
ALTER TABLE temporal_rng
ADD CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
CREATE UNLOGGED TABLE temporal_fk_rng2rng (
id int4range,
valid_at tsrange,
parent_id int4range,
CONSTRAINT temporal_fk_rng2rng_fk
FOREIGN KEY (parent_id, PERIOD valid_at)
REFERENCES temporal_rng (id, PERIOD valid_at)
ON UPDATE no action ON DELETE no action
DEFERRABLE
);
$$;
call overlap_template();

--- on update/delete restrict
-- coverage for TRI_FKey_restrict_upd,TRI_FKey_restrict_del.
BEGIN;
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(id,PERIOD valid_at) ON UPDATE RESTRICT ON
DELETE RESTRICT;

INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01',
'2021-01-01'));
INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01',
'2020-01-01'), '[11,11]');
savepoint s;

UPDATE temporal_rng FOR PORTION OF valid_at FROM '2018-01-01' TO '2018-01-03'
SET id = '[9,9]' WHERE id = '[11,11]';
ROLLBACK to s;
delete from  temporal_rng FOR PORTION OF valid_at FROM '2018-01-01' TO
'2020-01-01';
ROLLBACK to s;
--this one should not have error.
delete from  temporal_rng FOR PORTION OF valid_at FROM '2020-01-01' TO
'2021-01-01';
table temporal_rng;
ROLLBACK;

-------------
--- on delete set column list coverage for function tri_set. branch
{if (riinfo->ndelsetcols != 0)}
BEGIN;
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(id,PERIOD valid_at) ON DELETE set default(parent_id);

ALTER TABLE temporal_fk_rng2rng  ALTER COLUMN parent_id SET DEFAULT '[2,2]';
ALTER TABLE temporal_fk_rng2rng  ALTER COLUMN valid_at SET DEFAULT tsrange'(,)';
INSERT INTO temporal_rng VALUES ('[11,11]', tsrange('2018-01-01',
'2021-01-01'));
INSERT INTO temporal_fk_rng2rng VALUES ('[7,7]', tsrange('2018-01-01',
'2020-01-01'), '[11,11]');
insert into temporal_rng values('[2,2]','(,)');
savepoint s;
delete from  temporal_rng FOR PORTION OF valid_at FROM '2018-01-01' TO
'2019-01-01' where id = '[11,11]';
-- delete from  temporal_rng where id = '[11,11]';
table temporal_fk_rng2rng;
rollback;

---

From: jian he <[email-masked]>
Date: Mon, 30 Oct 2023 08:00:00 +0800

hi.

* The attached patch makes foreign keys with PERIOD fail if any of the
foreign key columns is "generated columns".

* The following queries will cause segmentation fault. not sure the
best way to fix it. the reason
in LINE: numpks = transformColumnNameList(RelationGetRelid(pkrel),
fkconstraint->pk_attrs, pkattnum, pktypoid);
begin;
drop table if exists temporal3,temporal_fk_rng2rng;
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, valid_at)
);

* change the function FindFKComparisonOperators's "eqstrategy"  to
make pg_constraint record correct {conpfeqop,conppeqop,conffeqop}.

* fix the ON DELETE SET NULL/DEFAULT (columnlist). Now the following
queries error will be more consistent.
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 DEFAULT(valid_at);
--ON DELETE SET NULL(valid_at);

* refactor restrict_cascading_range function.

* you did if (numfks != numpks) before if (is_temporal) {numfks +=
1;}, So I changed the code order to make the error report more
consistent.

anyway, I put it in one patch. please check the attached.
From 7dc2194f9bd46cfee7cfc774a2e52a2b64d465ea Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Sun, 29 Oct 2023 14:42:57 +0800
Subject: [PATCH v1 1/1] various small fixes.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* The attached patch makes foreign keys with PERIOD fail if any of the foreign key columns is "generated columns".
* change the function FindFKComparisonOperators's "eqstrategy"  to make pg_constraint record correct {conpfeqop,conppeqop,conffeqop}.
* refactor restrict_cascading_range function.
* first (is_temporal) {numfks += 1;} then (numfks != numpks) validation.
* variable comment fix.
---
 src/backend/commands/tablecmds.c    | 96 ++++++++++++++++-------------
 src/backend/utils/adt/ri_triggers.c | 32 +++++-----
 2 files changed, 69 insertions(+), 59 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5ae0f113..86e99c81 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -522,7 +522,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
 											bool is_temporal);
 static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 										 int numfksetcols, const int16 *fksetcolsattnums,
-										 List *fksetcols);
+										 const int16 *fkperiodattnums, List *fksetcols);
 static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
 									Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
 									int numfks, int16 *pkattnum, int16 *fkattnum,
@@ -10292,6 +10292,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 											  fkdelsetcols, NULL);
 	validateFkOnDeleteSetColumns(numfks, fkattnum,
 								 numfkdelsetcols, fkdelsetcols,
+								 fkperiodattnums,
 								 fkconstraint->fk_del_set_cols);
 
 	/*
@@ -10325,6 +10326,43 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 										   opclasses);
 	}
 
+	/*
+	 * On the strength of a previous constraint, we might avoid scanning
+	 * tables to validate this one.  See below.
+	 */
+	old_check_ok = (fkconstraint->old_conpfeqop != NIL);
+	Assert(!old_check_ok || numfks == list_length(fkconstraint->old_conpfeqop));
+
+	for (i = 0; i < numpks; i++)
+	{
+		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]);
+	}
+	/* set pfeq, ppeq, ffeq operators for withoutoverlaps constraint.
+	 * this also assume overlaps is the last key columns in constraint.
+	 *
+	*/
+	if (is_temporal)
+	{
+		pkattnum[numpks] = pkperiodattnums[0];
+		pktypoid[numpks] = pkperiodtypoids[0];
+		fkattnum[numpks] = fkperiodattnums[0];
+		fktypoid[numpks] = fkperiodtypoids[0];
+
+		FindFKComparisonOperators(
+				fkconstraint, tab, numpks, fkattnum,
+				&old_check_ok, &old_pfeqop_item,
+				pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks],
+				is_temporal, true,
+				&pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+		numfks += 1;
+		numpks += 1;
+	}
+
 	/*
 	 * Now we can check permissions.
 	 */
@@ -10371,38 +10409,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				(errcode(ERRCODE_INVALID_FOREIGN_KEY),
 				 errmsg("number of referencing and referenced columns for foreign key disagree")));
 
-	/*
-	 * On the strength of a previous constraint, we might avoid scanning
-	 * tables to validate this one.  See below.
-	 */
-	old_check_ok = (fkconstraint->old_conpfeqop != NIL);
-	Assert(!old_check_ok || numfks == list_length(fkconstraint->old_conpfeqop));
-
-	for (i = 0; i < numpks; i++)
-	{
-		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] = pkperiodattnums[0];
-		pktypoid[numpks] = pkperiodtypoids[0];
-		fkattnum[numpks] = fkperiodattnums[0];
-		fktypoid[numpks] = fkperiodtypoids[0];
-
-		FindFKComparisonOperators(
-				fkconstraint, tab, numpks, fkattnum,
-				&old_check_ok, &old_pfeqop_item,
-				pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks],
-				is_temporal, true,
-				&pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
-		numfks += 1;
-		numpks += 1;
-	}
-
 	/*
 	 * Create all the constraint and trigger objects, recursing to partitions
 	 * as necessary.  First handle the referenced side.
@@ -10455,11 +10461,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 							 int numfksetcols, const int16 *fksetcolsattnums,
-							 List *fksetcols)
+							const int16 *fkperiodattnums, List *fksetcols)
 {
 	for (int i = 0; i < numfksetcols; i++)
 	{
 		int16		setcol_attnum = fksetcolsattnums[i];
+		/* assume only one PERIOD key column in foreign key */
+		int16		fkperiod_attnum = fkperiodattnums[0];
 		bool		seen = false;
 
 		for (int j = 0; j < numfks; j++)
@@ -10469,6 +10477,14 @@ 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)
@@ -11502,7 +11518,7 @@ FindFKComparisonOperators(Constraint *fkconstraint,
 		 * For the non-overlaps parts, we want either RTEqualStrategyNumber (without btree_gist)
 		 * or BTEqualStrategyNumber (with btree_gist). We'll try the latter first.
 		 */
-		eqstrategy = for_overlaps ? RTOverlapStrategyNumber : BTEqualStrategyNumber;
+		eqstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber;
 	}
 	else
 	{
@@ -11521,19 +11537,11 @@ FindFKComparisonOperators(Constraint *fkconstraint,
 	/*
 	 * There had better be a primary equality operator for the index.
 	 * We'll use it for PK = PK comparisons.
+	 * This apply to gist and btree index method.
 	 */
 	ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
 								 eqstrategy);
 
-	/* Fall back to RTEqualStrategyNumber for temporal overlaps */
-	if (is_temporal && !for_overlaps && !OidIsValid(ppeqop))
-	{
-		eqstrategy = RTEqualStrategyNumber;
-		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);
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index d0027918..08b46536 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -245,7 +245,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 get_portion_intersect_range(const ForPortionOfState *tg_temporal,
 									  const RI_ConstraintInfo *riinfo,
 									  TupleTableSlot *oldslot);
 
@@ -816,7 +816,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 = get_portion_intersect_range(trigdata->tg_temporal, riinfo, oldslot);
 	}
 
 	ri_PerformCheck(riinfo, &qkey, qplan,
@@ -1435,7 +1435,7 @@ TRI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	 * 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);
+	targetRange = get_portion_intersect_range(trigdata->tg_temporal, riinfo, oldslot);
 
 	if (SPI_connect() != SPI_OK_CONNECT)
 		elog(ERROR, "SPI_connect failed");
@@ -1480,7 +1480,7 @@ TRI_FKey_cascade_del(PG_FUNCTION_ARGS)
 
 			quoteOneName(attname,
 						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
-			sprintf(paramname, "$%d", i + 1);
+			snprintf(paramname, sizeof(paramname), "$%d", i + 1);
 			ri_GenerateQual(&querybuf, querysep,
 							paramname, pk_type,
 							riinfo->pf_eq_oprs[i],
@@ -1596,7 +1596,7 @@ TRI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 	 * 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);
+	targetRange = get_portion_intersect_range(trigdata->tg_temporal, riinfo, oldslot);
 
 	if (SPI_connect() != SPI_OK_CONNECT)
 		elog(ERROR, "SPI_connect failed");
@@ -1621,9 +1621,11 @@ TRI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 		 *  UPDATE [ONLY] <fktable>
 		 *		  FOR PORTION OF $fkatt FROM lower(${2n+1}) TO upper(${2n+1})
 		 *		  SET fkatt1 = $1, [, ...]
-		 *		  WHERE $n = fkatt1 [AND ...]
+		 *		  WHERE $n + 1 = fkatt1 [AND ...]
 		 * The type id's for the $ parameters are those of the
-		 * corresponding PK attributes.  Note that we are assuming
+		 * corresponding PK attributes. ri_PerformCheck need fillin
+		 * oldslot and newslot key values. so we put targetRange(Portion)
+		 * to ${2n+1}. Note that we are assuming
 		 * there is an assignment cast from the PK to the FK type;
 		 * else the parser will fail.
 		 * ----------
@@ -1658,7 +1660,7 @@ TRI_FKey_cascade_upd(PG_FUNCTION_ARGS)
 								 "%s %s = $%d",
 								 querysep, attname, i + 1);
 
-			sprintf(paramname, "$%d", j + 1);
+			snprintf(paramname, sizeof(paramname), "$%d", j + 1);
 			ri_GenerateQual(&qualbuf, qualsep,
 							paramname, pk_type,
 							riinfo->pf_eq_oprs[i],
@@ -1793,7 +1795,7 @@ tri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	 * 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);
+	targetRange = get_portion_intersect_range(trigdata->tg_temporal, riinfo, oldslot);
 
 	if (SPI_connect() != SPI_OK_CONNECT)
 		elog(ERROR, "SPI_connect failed");
@@ -1910,7 +1912,7 @@ tri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 			quoteOneName(attname,
 						 RIAttName(fk_rel, riinfo->fk_attnums[i]));
 
-			sprintf(paramname, "$%d", i + 1);
+			snprintf(paramname, sizeof(paramname), "$%d", i + 1);
 			ri_GenerateQual(&querybuf, qualsep,
 							paramname, pk_type,
 							riinfo->pf_eq_oprs[i],
@@ -3877,17 +3879,17 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn
 }
 
 /*
- * restrict_cascading_range -
+ * get_portion_intersect_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.
+ * In a normal UPDATE/DELETE this should be the parent's own valid range,
+ * but if there was a FOR PORTION OF clause, then we should
+ * get the intersect of old valid range and FOR PORTION OF clause's range.
  */
 static Datum
-restrict_cascading_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
+get_portion_intersect_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot)
 {
 	Datum	pkRecordRange;
 	bool	isnull;
-- 
2.34.1

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 2 Nov 2023 13:21:37 -0700

Thanks for all the feedback! Consolidating several emails below:

 > On Fri, Oct 20, 2023 at 5:45 AM jian he <[email-masked]> 
wrote:
 > I don't think we need to check attr->attisdropped here

Changed.

 > "return *typnamespace;" should be "return true"?

No, but I added a comment to clarify.

 > Maybe name it get_typname_and_typnamespace?

I could go either way on this but I left it as-is since it seems 
redundant, and there are other functions here that don't repeat the 
three-letter prefix.

 > you can just `elog(ERROR, "missing range type %s", range_type_name);` ?

No, because this failure happens trying to look up the name.

 > Also, this should be placed just below if 
(!type_is_range(attr->atttypid))?

We ereport there (not elog) because it's a user error (using a 
non-rangetype for the option), not an internal error.

 > periods are always associated with the table, is the above else 
branch correct?

True but I'm following the code just above for OCLASS_CONSTRAINT. Even 
if this case is unexpected, it seems better to handle it gracefully than 
have a harder failure.

 > alter table pt add period for "........pg.dropped.4........" (ds, de);

Added, thanks!

 > On Sun, Oct 22, 2023 at 5:01 PM jian he <[email-masked]> 
 > CREATE unlogged TABLE for_portion_of_test1 (id int4range, valid_at

These are good tests, thanks! Originally FOR PORTION OF required a 
PRIMARY KEY or UNIQUE constraint, so we couldn't find NULLs here, but we 
changed that a while back, so it's good to verify it handles that case.

 > I wonder when this isNull will be invoked. The above tests won't
 > invoke the error.

As far as I can tell it shouldn't happen, which is why it's elog. The 
new tests don't hit it because a NULL range should never match the range 
in the FROM+TO of the FOR PORTION OF clause. Maybe this should even be 
an assert, but I think I prefer elog for the nicer error message and 
less-local condition.

 > OF "from and "to" both bound should not be null?

Correct, NULL and UNBOUNDED mean the same thing. This matches the 
meaning of NULL in ranges.

 > 4461: elog(ERROR, "Got a NULL FOR PORTION OF target range");

Here we're checking the "target range", in other words the range built 
from the FROM+TO of the FOR PORTION OF clause---not a range from a 
tuple. Finding a NULL here *for the range itself* would indeed be an 
error. A NULL *bound* means "unbounded", but a NULL *range* should not 
be possible to construct.

 > I also made some changes in the function range_leftover_internal,

I'm not really comfortable with these changes. "Leftover" doesn't refer 
to "left" vs "right" but to what *remains* (what is "left behind") after 
the UPDATE/DELETE. Also r1 and r2 are common parameter names throughout 
the rangetypes.c file, and they are more general than the names you've 
suggested. We shouldn't assume we will only ever call this function from 
the FOR PORTION OF context.

 > ExecForPortionOfLeftovers

Thanks! I've made these code changes (with slight modifications, e.g. no 
need to call ExecFetchSlotHeapTuple if there are no leftovers).

I'm not sure about the comment change though---I want to verify that 
myself (particularly the case when the partition key is updated so we 
have already been routed to a different partition than the old tuple).

 > On Tue, Oct 24, 2023 at 11:14 PM jian he 
<[email-masked]> wrote:
 > I refactored findNewOrOldColumn to better handle error reports.

Thanks, I like your changes here. Applied with some small adjustments.

 > On Sat, Oct 28, 2023 at 1:26 AM jian he <[email-masked]> 
wrote:
 > PERIOD column[s] will not be set to NULL in {ON DELETE|ON UPDATE}. . . .

I reworded this to explain that the PERIOD element will not be set to 
NULL (or the default value).

 > call overlaps_template();

I'm not sure I want to add indirection like this to the tests, which I 
think makes them harder to read (and update). But there is indeed a 
tough combinatorial explosion, especially in the foreign key tests. We 
want to cover {ON DELETE,ON UPDATE} {NO ACTION,RESTRICT,CASCADE,SET 
NULL,SET DEFAULT} when {child inserts,child updates,parent 
updates,parent deletes} with {one,two} scalar columns and {,not} 
partitioned. Also ON DELETE SET {NULL,DEFAULT} against only a subset of 
columns. I updated the test cases to delete and re-use the same id 
values, so at least they are more isolated and thus easier to edit. I 
also added tests for `(parent_id1, parent2, PERIOD valid_at)` cases as 
well as `ON DELETE SET {NULL,DEFAULT} (parent_id1)`. (I think that last 
case covers what you are trying to do here, but if I misunderstood 
please let me know.)

I haven't worked through your last email yet, but this seemed like 
enough changes to warrant an update.

New patches attached (rebased to 0bc726d9).

Yours,

From b80f819b9d0b38ae9d84ade731396e180e727ecc Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v17 1/5] 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.
---
 doc/src/sgml/catalogs.sgml                    |  12 +
 doc/src/sgml/ref/create_table.sgml            |  45 +-
 src/backend/catalog/heap.c                    |   2 +
 src/backend/catalog/index.c                   |  10 +-
 src/backend/catalog/pg_constraint.c           |   2 +
 src/backend/commands/indexcmds.c              | 155 ++++++-
 src/backend/commands/tablecmds.c              |  14 +-
 src/backend/commands/trigger.c                |   1 +
 src/backend/commands/typecmds.c               |   1 +
 src/backend/parser/gram.y                     |  30 +-
 src/backend/parser/parse_utilcmd.c            | 125 +++++-
 src/backend/utils/adt/ruleutils.c             |  29 +-
 src/backend/utils/cache/relcache.c            |  10 +-
 src/bin/pg_dump/pg_dump.c                     |  35 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  46 +++
 src/bin/psql/describe.c                       |  12 +-
 src/include/catalog/index.h                   |   1 +
 src/include/catalog/pg_constraint.h           |   9 +-
 src/include/commands/defrem.h                 |   3 +-
 src/include/nodes/parsenodes.h                |   4 +
 .../regress/expected/without_overlaps.out     | 386 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/without_overlaps.sql     | 282 +++++++++++++
 24 files changed, 1156 insertions(+), 61 deletions(-)
 create mode 100644 src/test/regress/expected/without_overlaps.out
 create mode 100644 src/test/regress/sql/without_overlaps.sql

index 3ec7391ec5..5412f2daa0 100644
 
+     <row>

 
 
      <para>
 
      <para>

      <para>
 
      <para>
 
index d03c961678..3e9cc4bb10 100644
 
 	pfree(ccbin);
@@ -2190,6 +2191,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
+							  false,	/* contemporal */
 							  false);
 	return constrOid;
 }
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 143fae01eb..2f1ec3b0ac 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -254,8 +254,8 @@ index_check_primary_key(Relation heapRel,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("primary keys cannot be expressions")));

 		atttuple = SearchSysCache2(ATTNUM,
@@ -1326,7 +1326,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId,
 											Anum_pg_index_indoption);
 	indcoloptions = (int2vector *) DatumGetPointer(colOptionDatum);
 
-	/* Fetch reloptions of index if any */
+	/* Fetch options of index if any */
 	classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(oldIndexId));
 	if (!HeapTupleIsValid(classTuple))
 		elog(ERROR, "cache lookup failed for relation %u", oldIndexId);
@@ -1895,6 +1895,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_TEMPORAL: constraint is for a temporal primary key
  * allow_system_table_mods: allow table to be a system catalog
  * is_internal: index is constructed due to internal process
  */
@@ -1918,11 +1919,13 @@ index_constraint_create(Relation heapRelation,
 	bool		mark_as_primary;
 	bool		islocal;
 	bool		noinherit;
+	bool		is_temporal;
 	int			inhcount;

 	/* constraint creation support doesn't work while bootstrapping */
 	Assert(!IsBootstrapProcessingMode());
@@ -1999,6 +2002,7 @@ index_constraint_create(Relation heapRelation,
 								   islocal,
 								   inhcount,
 								   noinherit,
+								   is_temporal,	/* contemporal */
 								   is_internal);
 
 	/*
 
 	if (conkeyArray)
index c160d8a301..e9d1b17935 100644
@@ -86,6 +91,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
@@ -143,6 +149,7 @@ typedef struct ReindexErrorInfo
@@ -172,7 +179,8 @@ bool
@@ -247,8 +255,8 @@ CheckIndexCompatible(Oid oldId,
 
 	/* Get the soon-obsolete pg_index tuple. */
 	tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -558,6 +566,7 @@ DefineIndex(Oid tableId,
 	bool		amcanorder;
 	bool		amissummarizing;
 	amoptions_function amoptions;
+	bool		exclusion;
 	bool		partitioned;
 	bool		safe_index;
 	Datum		reloptions;
@@ -676,6 +685,12 @@ DefineIndex(Oid tableId,

+	/*
@@ -844,7 +859,7 @@ DefineIndex(Oid tableId,
 
@@ -859,7 +874,7 @@ DefineIndex(Oid tableId,
@@ -912,8 +927,9 @@ DefineIndex(Oid tableId,
 
 	/*
 	 * Extra checks when creating a PRIMARY KEY index.
@@ -931,7 +947,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;
@@ -939,6 +955,8 @@ DefineIndex(Oid tableId,
 
 		if (stmt->primary)
@@ -985,10 +1003,10 @@ DefineIndex(Oid tableId,
@@ -1021,12 +1039,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -1035,7 +1053,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1179,6 +1197,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+	if (stmt->istemporal)
+		constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1847,6 +1867,91 @@ CheckPredicate(Expr *predicate)
 				 errmsg("functions in index predicate must be marked IMMUTABLE")));
 }
 
+/*
@@ -1870,6 +1975,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool istemporal,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1893,6 +1999,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;

@@ -2169,6 +2283,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 
 		/*
index 416a98e7ce..7c627d6a66 100644
@@ -10084,6 +10085,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10382,6 +10384,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conTemporal */
 									  false);
 
 			/*
@@ -10887,6 +10890,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conTemporal */
 								  true);
 
@@ -11561,6 +11565,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
@@ -11727,10 +11732,12 @@ transformColumnNameList(Oid relId, List *colList,
@@ -14272,7 +14279,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)

index aaf9728697..1773556053 100644
index c224df4ecc..14d4e24fcc 100644
@@ -4104,7 +4105,7 @@ ConstraintElem:
@@ -4113,11 +4114,12 @@ ConstraintElem:
@@ -4138,7 +4140,7 @@ ConstraintElem:
@@ -4146,11 +4148,12 @@ ConstraintElem:
@@ -4228,6 +4231,11 @@ columnList:
 
+without_overlaps_clause:
+			',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+			| /*EMPTY*/               { $$ = NULL; }
+	;
+
 columnElem: ColId
 				{
 					$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index cf0d432ab1..f4705ecc2b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,6 +124,7 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
 static void transformIndexConstraints(CreateStmtContext *cxt);
 static IndexStmt *transformIndexConstraint(Constraint *constraint,
 										   CreateStmtContext *cxt);
+static void validateWithoutOverlaps(CreateStmtContext *cxt, char *colname, int location);
 static void transformExtendedStatistics(CreateStmtContext *cxt);
 static void transformFKConstraints(CreateStmtContext *cxt,
 								   bool skipValidation,
@@ -1716,6 +1717,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	index->unique = idxrec->indisunique;
 	index->nulls_not_distinct = idxrec->indnullsnotdistinct;
 	index->primary = idxrec->indisprimary;
+	index->istemporal = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
 	index->transformed = true;	/* don't need transformIndexStmt */
 	index->concurrent = false;
 	index->if_not_exists = false;
@@ -1765,7 +1767,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			nElems;
 				int			i;
 
@@ -2305,6 +2309,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 
@@ -2397,6 +2402,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 					 errmsg("index \"%s\" is not valid", index_name),
 					 parser_errposition(cxt->pstate, constraint->location)));
 
+		/*
@@ -2673,6 +2683,52 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
+			 * Raise an error if we can't find the column named in WITHOUT OVERLAPS,
+			 * or if it's not a range type.
+			validateWithoutOverlaps(cxt, without_overlaps_str, constraint->location);
+				 * The loop above does not include the WITHOUT OVERLAPS attribute,
+				 * so we must do it here.
 
 	/*
@@ -2792,6 +2848,73 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	return index;
 }
 
+/*
+ * validateWithoutOverlaps -
+ *
+ * and the new ones. Raises an error if we can't find one or it's not a range type.
+validateWithoutOverlaps(CreateStmtContext *cxt, char *colname, int location)
+								colname),
+						 parser_errposition(cxt->pstate, location)));
+									colname),
+							 parser_errposition(cxt->pstate, location)));
+	/* no such column, report an error */
+			 errmsg("column \"%s\" named in key does not exist", colname),
+			 parser_errposition(cxt->pstate, location)));
index ed7f40f053..f2da27eb50 100644

 

 

 

 
 		if (j == 0)
index a49ab465b3..8925e8cb7d 100644

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e863913849..54a15cf64a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6989,7 +6989,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_tablespace,
 				i_indreloptions,
 				i_indstatcols,
-				i_indstatvals;
+				i_indstatvals,
+				i_withoutoverlaps;
 
 	/*
 	 * We want to perform just one query against pg_index.  However, we
@@ -7064,10 +7065,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7142,6 +7150,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

@@ -7244,6 +7253,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
 			}
@@ -16939,9 +16949,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 					break;
 				attname = getAttrName(indkey, tbinfo);

index 2fe3cbed9a..ad7cc85b8c 100644
 
index eb3ec534b4..eb35d5ef60 100644

index a4770eaf12..1a858c3897 100644
@@ -92,6 +92,7 @@ extern Oid	index_create(Relation heapRelation,
 
index a026b42515..98a560741d 100644
 
+	/*
 
 	/*
 
 	/*

index cf7e79062e..bd5068670a 100644
 
index 0000000000..36aebc452f
@@ -0,0 +1,386 @@
+LINE 3:  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU...
+LINE 4:  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU...
+-- We don't drop this table because tests below also need multiple scalar columns.
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+LINE 3:  CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLA...
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+LINE 3:  CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV...
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+LINE 4:  CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV...
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+    "temporal_rng3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+ CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id, valid_at)
+-- UNIQUE with a custom range type:
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
+NOTICE:  ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "idx_temporal3_uq" to "temporal3_uq"
index f0987ff537..16683148d2 100644
 
 # ----------
index 0000000000..bf1151ab45
@@ -0,0 +1,282 @@
+-- We don't drop this table because tests below also need multiple scalar columns.
+	CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
+	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+-- UNIQUE with a custom range type:
+	CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq;
+-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint):
+CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at);
2.42.0

From 26dd05aa3b7bf3de61edd502b45a9ee2ae765429 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v17 2/5] 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.
---
 doc/src/sgml/ref/create_table.sgml            |  48 +-
 src/backend/commands/tablecmds.c              | 777 +++++++++++-------
 src/backend/parser/gram.y                     |  37 +-
 src/backend/utils/adt/ri_triggers.c           | 342 +++++++-
 src/backend/utils/adt/ruleutils.c             |  19 +-
 src/include/catalog/pg_constraint.h           |   8 +-
 src/include/catalog/pg_proc.dat               |  24 +
 src/include/nodes/parsenodes.h                |   2 +
 src/include/parser/kwlist.h                   |   1 +
 .../regress/expected/without_overlaps.out     | 510 ++++++++++++
 src/test/regress/sql/without_overlaps.sql     | 488 +++++++++++
 11 files changed, 1913 insertions(+), 343 deletions(-)

index ad5bf21453..3329a8dea1 100644
 
@@ -1246,6 +1264,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
           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>
 
@@ -1257,6 +1281,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>
 
@@ -1270,6 +1301,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
+          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.
index 7c627d6a66..c6cc07838a 100644
@@ -5899,7 +5911,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
@@ -9519,6 +9532,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9613,6 +9631,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9631,7 +9653,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9639,8 +9663,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9699,187 +9729,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
 	/*
@@ -9898,7 +9768,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
@@ -9914,7 +9785,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -9999,7 +9871,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10085,7 +9958,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10161,7 +10034,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10219,7 +10093,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10267,6 +10142,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10384,7 +10260,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conTemporal */
+									  is_temporal,	/* conTemporal */
 									  false);
 
 			/*
@@ -10415,7 +10291,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10651,7 +10528,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->contemporal);
 
@@ -10890,7 +10768,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
@@ -10924,13 +10802,245 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)

+static void
@@ -11428,7 +11538,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11676,7 +11790,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11744,7 +11857,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;
@@ -11809,36 +11924,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11865,6 +11999,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11886,12 +12024,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11929,6 +12071,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12038,7 +12193,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12067,8 +12223,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;
 
 	/*
@@ -12137,6 +12295,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12156,12 +12315,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12219,37 +12384,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12279,37 +12475,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
index 14d4e24fcc..301a4fa221 100644

@@ -4194,21 +4195,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
@@ -4236,6 +4239,16 @@ without_overlaps_clause:
 
+optionalPeriodName:
@@ -17559,6 +17572,7 @@ reserved_keyword:
@@ -17868,6 +17882,7 @@ bare_label_keyword:

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

 

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

 

 

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

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

index f2da27eb50..9d5c07c96f 100644

 

 

index 98a560741d..a187bed245 100644
 
 	/*
 
 	/*
 
 	/*

 
index 091f7e343c..1729d1d3dd 100644
@@ -3945,6 +3945,30 @@
 
index bd5068670a..116b308dbf 100644
index 36aebc452f..4df6cad364 100644
@@ -384,3 +384,513 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
+-- Two scalar columns
+	parent_id1 int4range,
+	parent_id2 int4range,
+	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"
+------------+-----------+-----------+----------+---------
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+    "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)
+-- Two scalar columns:
+	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"
+------------+-----------+-----------+----------+---------
+ parent_id1 | int4range |           |          | 
+ parent_id2 | int4range |           |          | 
+    "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)
index bf1151ab45..6065ef264c 100644
@@ -280,3 +280,491 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+-- Two scalar columns
+	parent_id1 int4range,
+	parent_id2 int4range,
+	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
+-- Two scalar columns:
+	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
2.42.0

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

 src/backend/executor/nodeModifyTable.c        | 226 +++++++-
 src/backend/parser/analyze.c                  | 211 +++++++-
 src/backend/utils/adt/period.c                |  56 ++
 src/backend/utils/cache/lsyscache.c           |  53 ++
 src/include/nodes/execnodes.h                 |  27 +
 src/test/regress/expected/for_portion_of.out  | 498 ++++++++++++++++++
 .../regress/expected/without_overlaps.out     | 144 ++++-
 src/test/regress/sql/for_portion_of.sql       | 371 +++++++++++++
 src/test/regress/sql/without_overlaps.sql     |  93 +++-
 47 files changed, 2287 insertions(+), 51 deletions(-)

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

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c6cc07838a..e32e05e306 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12266,6 +12266,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

 

 

 
 
+/* ----------
+ * ForPortionOfState()
+ *
 
index 299c2c75be..5e2374cfa2 100644

@@ -1205,6 +1212,142 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+	HeapTuple oldtuple = NULL;
+	bool shouldFree = false;
+		if (!oldtuple)
@@ -1357,7 +1500,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1390,6 +1534,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -2133,6 +2282,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -3610,6 +3764,7 @@ ExecModifyTable(PlanState *pstate)
@@ -4283,6 +4438,75 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
 	 * be treated like non-locked relations in SELECT FOR UPDATE, i.e., the

index a8cea5efe1..48af49f0bd 100644
@@ -1963,6 +1963,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
 
index 0b1d17b9d3..0e04490796 100644
@@ -3686,7 +3686,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3752,6 +3753,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
index 7a1dfb6364..16c6498049 100644
 
@@ -60,10 +62,17 @@

+static Node *

 
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1231,158 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2589,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2426,6 +2607,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2442,7 +2627,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2451,7 +2637,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2470,7 +2656,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2520,6 +2706,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 301a4fa221..2753527e27 100644

 
 	QUOTE
 
+/*
@@ -12203,14 +12215,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;
 				}
@@ -12273,6 +12287,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
@@ -12281,10 +12296,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;
 				}
@@ -13720,6 +13736,19 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+for_portion_of_clause:
@@ -17282,6 +17311,7 @@ unreserved_keyword:
@@ -17886,6 +17916,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
index c480ce3682..c63301f0f7 100644
 
 			/*
 
 			/*
index 6c29471bb3..cef0bec46d 100644
 
 			/*
index 41a362310a..2662ad84ce 100644
@@ -3459,6 +3459,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
@@ -3797,6 +3821,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
 
 					/*
index 24bad52923..43a580b5ae 100644
 
+/*
 
 Datum

 /*

 

 

 	/*
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * fpo_targets_pk_range
+ *
index fc6d267e44..db245e65ad 100644
@@ -2170,6 +2170,59 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
+ * get_typname
+ *
+		/* *typnamespace is NULL if it wasn't found: */
 
 /*
index 5d7f17dee0..47a4ab3dad 100644
 
 struct PlanState;				/* forward references in this file */
 struct ParallelHashJoinState;
@@ -421,6 +423,28 @@ typedef struct MergeActionState
 	ExprState  *mas_whenqual;	/* WHEN [NOT] MATCHED AND conditions */
 } MergeActionState;
 
+/*
+ * ForPortionOfState
+ *
@@ -541,6 +565,9 @@ typedef struct ResultRelInfo

index 116b308dbf..293500d480 100644

+/*
+ * ForPortionOfClause

index ed85dc7414..54acadc8fc 100644
index 24d46c76dc..e65b71ae98 100644
@@ -244,6 +244,7 @@ typedef struct ModifyTable
index ab6d7fdc6d..567c050700 100644
 
+/*----------
 
index c22cabdf42..88e49db3d8 100644
 
index 0000000000..224fe1011d
@@ -0,0 +1,498 @@
+-- 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.
+CREATE UNLOGGED TABLE for_portion_of_test (
+  name text
+  ('[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');
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'NULL to NULL';
+-------+---------------------------------------------------------+--------------
+ [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)
+  FOR PORTION OF valid_at FROM NULL TO UNBOUNDED
+  SET name = 'NULL to UNBOUNDED';
+-------+---------------------------------------------------------+-------------------
+ [1,2) |                                                         | 1 null
+ [1,2) | empty                                                   | 1 empty
+       |                                                         | 
+ [1,2) | (,)                                                     | NULL to UNBOUNDED
+       | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | NULL to UNBOUNDED
+(5 rows)
+  FOR PORTION OF valid_at FROM UNBOUNDED TO NULL
+  SET name = 'UNBOUNDED to NULL';
+-------+---------------------------------------------------------+-------------------
+ [1,2) |                                                         | 1 null
+ [1,2) | empty                                                   | 1 empty
+       |                                                         | 
+ [1,2) | (,)                                                     | UNBOUNDED to NULL
+       | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | UNBOUNDED to NULL
+(5 rows)
+  FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+  SET name = 'UNBOUNDED to UNBOUNDED';
+-------+---------------------------------------------------------+------------------------
+ [1,2) |                                                         | 1 null
+ [1,2) | empty                                                   | 1 empty
+       |                                                         | 
+ [1,2) | (,)                                                     | UNBOUNDED to UNBOUNDED
+       | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | UNBOUNDED to UNBOUNDED
+(5 rows)
index a73c1f90c4..727ad08e08 100644
@@ -3020,6 +3020,38 @@ select * from uv_iocu_tab;
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 4df6cad364..2c6d468051 100644
@@ -296,6 +296,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
@@ -348,6 +378,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -383,6 +443,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -632,13 +722,25 @@ UPDATE temporal_rng SET id = '[7,7]'
 -- test FK parent updates RESTRICT
@@ -669,13 +771,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -697,9 +811,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
 -- test FK parent deletes RESTRICT
@@ -721,9 +848,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
+DELETE FROM temporal_rng WHERE id IN ('[5,5]');
index 16683148d2..7b9b99e0a3 100644
 
 # ----------
index 0000000000..c447e23a2e
@@ -0,0 +1,371 @@
+-- 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.
+CREATE UNLOGGED TABLE for_portion_of_test (
+  name text
+  ('[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');
+  FOR PORTION OF valid_at FROM NULL TO NULL
+  SET name = 'NULL to NULL';
+  FOR PORTION OF valid_at FROM NULL TO UNBOUNDED
+  SET name = 'NULL to UNBOUNDED';
+  FOR PORTION OF valid_at FROM UNBOUNDED TO NULL
+  SET name = 'UNBOUNDED to NULL';
+  FOR PORTION OF valid_at FROM UNBOUNDED TO UNBOUNDED
+  SET name = 'UNBOUNDED to UNBOUNDED';

index 6065ef264c..4603362c50 100644
@@ -223,6 +223,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
 
 --
@@ -261,6 +275,22 @@ INSERT INTO temporal_partitioned VALUES
 
 -- temporal UNIQUE:
@@ -279,6 +309,22 @@ INSERT INTO temporal_partitioned VALUES
 
 --
@@ -501,13 +547,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 
 --
 -- test FK parent updates RESTRICT
@@ -536,13 +592,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -562,14 +628,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
+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
@@ -586,9 +662,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]');
 
2.42.0

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

---
 src/backend/commands/tablecmds.c              |  28 +-
 src/backend/utils/adt/ri_triggers.c           | 549 +++++++++++-
 src/include/catalog/pg_proc.dat               |  20 +
 .../regress/expected/without_overlaps.out     | 808 +++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 421 ++++++++-
 5 files changed, 1788 insertions(+), 38 deletions(-)

index e32e05e306..54b484ac5f 100644
@@ -12401,11 +12401,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12492,11 +12500,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*
 	 * Use the query type code to determine whether the query is run against
@@ -2588,8 +3125,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
 	{
@@ -3300,8 +3839,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 1729d1d3dd..b0d1c90c9c 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 2c6d468051..327a753f86 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -867,30 +867,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,
@@ -898,7 +1052,442 @@ 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]');
+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]');
+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,
@@ -906,8 +1495,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'),
@@ -920,8 +1509,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]'),
@@ -956,7 +1545,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]';
@@ -968,7 +1557,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]';
@@ -990,7 +1579,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]';
@@ -1002,35 +1591,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 4603362c50..79135f39b6 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -680,31 +680,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,
@@ -712,6 +795,254 @@ 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]');
+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]');
+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;

@@ -721,8 +1052,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -736,8 +1067,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -821,37 +1152,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 
 
2.42.0

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

 src/backend/commands/tablecmds.c              | 763 +++++++++++++++++-
 src/test/regress/expected/periods.out         | 177 ++++
 src/test/regress/sql/periods.sql              | 119 +++
 48 files changed, 2316 insertions(+), 44 deletions(-)

index 5412f2daa0..f632e427cf 100644
 
+     <row>
@@ -5725,6 +5730,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l

diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9c39e9aab5..8412bf0a87 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>

 
index 3329a8dea1..9fae1540da 100644

 
+  <para>
 
index 3ce6c09b44..5dd78c7d09 100644
@@ -2817,6 +2817,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
@@ -2958,6 +2959,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index 3e9cc4bb10..99e201b45b 100644
 
+/*

 /*
index bb4efcad20..b48895b355 100644
 
+		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 bf47b0f6e2..dc532a39e7 100644
@@ -1138,6 +1138,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
@@ -1196,6 +1197,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
@@ -2237,6 +2239,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2321,6 +2324,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index 54b484ac5f..f30418d936 100644
 
 static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
 static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);

+	/*
@@ -1275,6 +1314,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1391,6 +1445,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3347,6 +3659,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4403,12 +4877,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4417,7 +4891,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4510,6 +4984,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4825,6 +5301,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5222,6 +5706,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6366,6 +6858,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6389,6 +6883,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7394,14 +7890,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7445,6 +7956,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -8089,6 +8670,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -13598,6 +14328,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15661,7 +16400,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index 2753527e27..22a4ab1f4a 100644
 
@@ -3720,8 +3738,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4073,6 +4093,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7131,6 +7164,14 @@ CommentStmt:

index f4705ecc2b..e96e944629 100644
 
 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,
@@ -241,6 +245,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;
@@ -281,6 +286,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 				transformColumnDefinition(&cxt, (ColumnDef *) element);
 				break;
 
+			case T_PeriodDef:
@@ -348,6 +357,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
@@ -914,6 +924,113 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
@@ -1004,6 +1121,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1056,6 +1174,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1065,10 +1184,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
@@ -3105,6 +3232,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.)
@@ -3562,6 +3693,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;
@@ -3624,6 +3756,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
index db245e65ad..12ac689a1f 100644

 /*
@@ -3533,6 +3596,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
 
 /*
index 256d1e35a4..0a2696c559 100644
@@ -3518,6 +3518,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
index 54a15cf64a..343331be10 100644
@@ -6350,6 +6350,7 @@ getTables(Archive *fout, int *numTables)
@@ -6427,6 +6428,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6564,6 +6573,7 @@ getTables(Archive *fout, int *numTables)
@@ -6647,6 +6657,7 @@ getTables(Archive *fout, int *numTables)
@@ -8381,7 +8392,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8434,6 +8445,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8448,7 +8461,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8984,15 +8998,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9014,6 +9049,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9033,12 +9069,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9097,6 +9134,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10367,6 +10477,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15902,6 +16014,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -15910,7 +16048,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16128,7 +16266,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16430,7 +16568,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18526,6 +18664,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 ad7cc85b8c..2eb0c951ef 100644

 

+		/* print periods */
index abac0f6da5..daecf44671 100644

 
 /*
  * prototypes for functions in pg_range.c
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index a9c6825601..732840ce6c 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -40,7 +40,8 @@ extern LOCKMODE AlterTableGetLockLevel(List *cmds);

 
 
@@ -105,5 +106,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 */
index 293500d480..59fec10ce5 100644

+
+/* ----------
 
index 88e49db3d8..1e32876188 100644
index 5d47a652cc..dc8646f5f1 100644
index 0000000000..6481c9b305
@@ -0,0 +1,177 @@
+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
index 7b9b99e0a3..b1cd676d96 100644
 
 # ----------
index 0000000000..f0be890217
@@ -0,0 +1,119 @@
+alter table pt add period for tableoid (ds, de);
+alter table pt add period for "........pg.dropped.4........" (ds, de);
2.42.0

---

From: jian he <[email-masked]>
Date: Tue, 7 Nov 2023 15:07:15 +0800

hi. based on v17. I found several doc related issues. previously I
didn't look closely....

+         </para>

The first "para" should be <para> ?
---
There are many warnings after #define WRITE_READ_PARSE_PLAN_TREES
see: http://cfbot.cputube.org/highlights/all.html#4308
Does that mean oue new change in gram.y is somehow wrong?
------
sgml/html/sql-update.html:
"range_or_period_name
The range column or period to use when performing a temporal update.
This must match the range or period used in the table's temporal
primary key."

Is the second sentence unnecessary? since no primary can still do "for
portion of update".

sgml/html/sql-update.html:
"start_time
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
range_or_period_name. It may also be the special value MINVALUE to
indicate an update whose beginning is unbounded."

probably something like the following:
"lower_bound"
The lower bound (inclusive) to change in an overlap update. This must
be a value matching the base type of the range or period from
range_or_period_name. It may also be the special value UNBOUNDED to
indicate an update whose beginning is unbounded."

Obviously the "start_time" reference also needs to change, and the
sql-delete.html reference also needs to change.
----------------------------------
UPDATE for_portion_of_test FOR PORTION OF valid_at  FROM NULL TO
"unbounded" SET name = 'NULL to NULL';
should fail, but not. double quoted unbounded is a column reference, I assume.

That's why I am confused with the function transformForPortionOfBound.
"if (nodeTag(n) == T_ColumnRef)" part.
-----------------------------------
in create_table.sgml. you also need to add  WITHOUT OVERLAPS related
info into <varlistentry id="sql-createtable-parms-unique">

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 9 Nov 2023 14:47:12 +0100

> New patches attached (rebased to 0bc726d9).

I went over the patch 
v17-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch in more 
detail.  Attached is a fixup patch that addresses a variety of cosmetic 
issues.

Some details:

- Renamed contemporal to conwithoutoverlaps, as previously discussed. 
Also renamed various variables and function arguments similarly.

- Rearranged text in CREATE TABLE reference page so there are no forward 
references.  (Describe WITHOUT OVERLAPS under UNIQUE and then PRIMARY 
KEy says "see above", rather than describe it under PRIMARY KEY and have 
UNIQUE say "see below.)

- Removed various bits that related to temporal foreign keys, which 
belong in a later patch.

- Reverted some apparently unrelated changes in src/backend/catalog/index.c.

- Removed the "temporal UNIQUE" constraint_type assignment in 
DefineIndex().  This is meant to be used in error messages and should 
refer to actual syntax.  I think it's fine without it this change.

- Field contemporal in NewConstraint struct is not used by this patch.

- Rearrange the grammar so that the rule with WITHOUT OVERLAPS is just a 
Boolean attribute rather than column name plus keywords.  This was kind 
of confusing earlier and led to weird error messages for invalid syntax. 
  I kept the restriction that you need at least one non-overlaps column, 
but that is now enforced in parse analysis, not in the grammar.  (But 
maybe we don't need it?)

(After your earlier explanation, I'm content to just allow one WITHOUT 
OVERLAPS column for now.)

- Some places looked at conexclop to check whether something is a 
WITHOUT OVERLAPS constraint, instead of looking at conwithoutoverlaps 
directly.

- Removed some redundant "unlike" entries in the pg_dump tests.  (This 
caused cfbot tests to fail.)

- Moved the "without_overlaps" test later in the schedule.  It should at 
least be after "constraints" so that normal constraints are tested first.

Two areas that could be improved:

1) In src/backend/commands/indexcmds.c, 
get_index_attr_temporal_operator() has this comment:

This doesn't see very confident. ;-)  I don't quite understand this.  Is 
this a gap in the currently available APIs, do we need to improve 
something here, or does this need more research?

2) In src/backend/parser/parse_utilcmd.c, transformIndexConstraint(), 
there is too much duplication between the normal and the if 
(constraint->without_overlaps) case, like the whole not-null constraints 
stuff at the end.  This should be one code block with a few conditionals 
inside.  Also, the normal case deals with things like table inheritance, 
which the added parts do not.  Is this all complete?

I'm not sure the validateWithoutOverlaps() function is needed at this 
point in the code.  We just need to check that the column exists, which 
the normal code path already does, and then have the index creation code 
later check that an appropriate overlaps operator exists.  We don't even 
need to restrict this to range types.  Consider for example, it's 
possible that a type does not have a btree equality operator.  We don't 
check that here either, but let the index code later check it.

Overall, with these fixes, I think this patch is structurally okay.  We 
just need to make sure we have all the weird corner cases covered.
From 6d7af1f78505c08fb205a56bd1ba3cb951f04002 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Thu, 9 Nov 2023 14:11:34 +0100
Subject: [PATCH] fixup! Add temporal PRIMARY KEY and UNIQUE constraints

---
 doc/src/sgml/catalogs.sgml                    |  8 +--
 doc/src/sgml/ref/create_table.sgml            | 68 ++++++++-----------
 src/backend/catalog/heap.c                    |  4 +-
 src/backend/catalog/index.c                   | 14 ++--
 src/backend/catalog/pg_constraint.c           |  4 +-
 src/backend/commands/indexcmds.c              | 30 ++++----
 src/backend/commands/tablecmds.c              | 16 ++---
 src/backend/commands/trigger.c                |  2 +-
 src/backend/commands/typecmds.c               |  2 +-
 src/backend/parser/gram.y                     | 19 +++---
 src/backend/parser/parse_utilcmd.c            | 28 ++++++--
 src/backend/utils/adt/ruleutils.c             | 27 +++-----
 src/backend/utils/cache/relcache.c            | 16 +++--
 src/bin/pg_dump/pg_dump.c                     | 33 +++------
 src/bin/pg_dump/pg_dump.h                     |  2 +-
 src/bin/pg_dump/t/002_pg_dump.pl              | 14 ++--
 src/bin/psql/describe.c                       | 10 +--
 src/include/catalog/index.h                   |  2 +-
 src/include/catalog/pg_constraint.h           | 11 +--
 src/include/commands/defrem.h                 |  2 +-
 src/include/nodes/parsenodes.h                |  6 +-
 .../regress/expected/without_overlaps.out     |  8 +--
 src/test/regress/parallel_schedule            |  4 +-
 23 files changed, 148 insertions(+), 182 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 5412f2daa0..618a05ac89 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2711,13 +2711,11 @@ <title><structname>pg_constraint</structname> Columns</title>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>contemporal</structfield> <type>bool</type>
+       <structfield>conwithoutoverlaps</structfield> <type>bool</type>
       </para>
       <para>
-       This constraint is a application-time temporal constraint,
-       defined with <literal>WITHOUT OVERLAPS</literal> (for primary
-       key and unique constraints) or <literal>PERIOD</literal> (for
-       foreign keys).
+       This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
+       (for primary keys and unique constraints).
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index ad5bf21453..a0bd2b8adc 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -78,8 +78,8 @@
 [ 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">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
-  PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</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> 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
@@ -107,10 +107,6 @@

-<phrase><replaceable class="parameter">temporal_interval</replaceable> in a <literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>, or <literal>FOREIGN KEY</literal> constraint is:</phrase>
-
-<replaceable class="parameter">range_column_name</replaceable>
-
 <phrase><replaceable class="parameter">referential_action</replaceable> in a <literal>FOREIGN KEY</literal>/<literal>REFERENCES</literal> constraint is:</phrase>
 
 { NO ACTION | RESTRICT | CASCADE | SET NULL [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | SET DEFAULT [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] }
@@ -969,7 +965,7 @@ <title>Parameters</title>
 
    <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>
@@ -983,6 +979,26 @@ <title>Parameters</title>
       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 type.  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
@@ -1007,8 +1023,7 @@ <title>Parameters</title>
       Adding a unique constraint will automatically create a unique btree
       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 and behave like a temporal <literal>PRIMARY
-      KEY</literal>: preventing duplicates only in overlapping time periods.
+      it will use a GiST index.
      </para>
 
      <para>
@@ -1026,8 +1041,7 @@ <title>Parameters</title>
 
    <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> [, ... ]
-    [, <replaceable class="parameter">temporal_interval</replaceable> WITHOUT OVERLAPS ] )</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>
@@ -1060,9 +1074,10 @@ <title>Parameters</title>
      </para>
 
      <para>
-      Adding a <literal>PRIMARY KEY</literal> constraint will automatically
-      create a unique btree index (or GiST if temporal) 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>
@@ -1075,31 +1090,6 @@ <title>Parameters</title>
       (e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
       index deletion.
      </para>
-
-     <para>
-      A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
-      is a <emphasis>temporal</emphasis> primary key.
-      The <literal>WITHOUT OVERLAPS</literal> column
-      must be a range type or a <literal>PERIOD</literal>
-      and is used to constrain the record's applicability
-      to just that range/period (usually a range of dates or timestamps,
-      although Postgres allows a range/period over any base type).
-     </para>
-
-     <para>
-      The non-<literal>WITHOUT OVERLAPS</literal> part(s) of the key 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).
-      These ordinary parts of the primary key do allow duplicates,
-      so long as the duplicates don't overlap in the
-      <literal>WITHOUT OVERLAPS</literal> column.
-      In effect a temporal <literal>PRIMARY KEY</literal> is enforced with an
-      <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
-      constraint. So for example <literal>PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)</literal>
-      behaves like <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH &amp;&amp;)</literal>.
-     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 3e9cc4bb10..f126d8e676 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2140,7 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 							  is_local, /* conislocal */
 							  inhcount, /* coninhcount */
 							  is_no_inherit,	/* connoinherit */
-							  false,	/* contemporal */
+							  false,	/* conwithoutoverlaps */
 							  is_internal); /* internally constructed? */
 
 	pfree(ccbin);
@@ -2191,7 +2191,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
-							  false,	/* contemporal */
+							  false,	/* conwithoutoverlaps */
 							  false);
 	return constrOid;
 }
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 2f1ec3b0ac..8444d768e5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -254,8 +254,8 @@ index_check_primary_key(Relation heapRel,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("primary keys cannot be expressions")));
 
-		/* System attributes are never null, so no need to check. */
-		if (attnum <= 0)
+		/* System attributes are never null, so no need to check */
+		if (attnum < 0)
 			continue;

-	/* Fetch options of index if any */
+	/* Fetch reloptions of index if any */
 	classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(oldIndexId));
 	if (!HeapTupleIsValid(classTuple))
 		elog(ERROR, "cache lookup failed for relation %u", oldIndexId);
@@ -1895,7 +1895,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_TEMPORAL: constraint is for a temporal primary key
+ *		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
  */
@@ -1919,13 +1919,13 @@ index_constraint_create(Relation heapRelation,
 	bool		mark_as_primary;
 	bool		islocal;
 	bool		noinherit;
-	bool		is_temporal;
+	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_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
+	is_without_overlaps = (constr_flags & INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS) != 0;
 
 	/* constraint creation support doesn't work while bootstrapping */
 	Assert(!IsBootstrapProcessingMode());
@@ -2002,7 +2002,7 @@ index_constraint_create(Relation heapRelation,
 								   islocal,
 								   inhcount,
 								   noinherit,
-								   is_temporal,	/* contemporal */
+								   is_without_overlaps,
 								   is_internal);
 
 	/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index d5329ca8c9..ffd340a2e4 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -78,7 +78,7 @@ CreateConstraintEntry(const char *constraintName,
 					  bool conIsLocal,
 					  int conInhCount,
 					  bool conNoInherit,
-					  bool conTemporal,
+					  bool conWithoutOverlaps,
 					  bool is_internal)
 {
 	Relation	conDesc;
@@ -194,7 +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_contemporal - 1] = BoolGetDatum(conTemporal);
+	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 e9d1b17935..d56db81fa1 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -91,7 +91,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
 							  Oid accessMethodId,
 							  bool amcanorder,
 							  bool isconstraint,
-							  bool istemporal,
+							  bool iswithoutoverlaps,
 							  Oid ddl_userid,
 							  int ddl_sec_context,
 							  int *ddl_save_nestlevel);
@@ -149,7 +149,7 @@ typedef struct ReindexErrorInfo
  *		to index on.
  * 'exclusionOpNames': list of names of exclusion-constraint operators,
  *		or NIL if not an exclusion constraint.
- * 'istemporal': true iff this index has a WITHOUT OVERLAPS clause.
+ * '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
@@ -180,7 +180,7 @@ CheckIndexCompatible(Oid oldId,
 					 const char *accessMethodName,
 					 const List *attributeList,
 					 const List *exclusionOpNames,
-					 bool istemporal)
+					 bool isWithoutOverlaps)
 {
 	bool		isconstraint;
 	Oid		   *typeIds;
@@ -255,7 +255,7 @@ CheckIndexCompatible(Oid oldId,
 					  coloptions, attributeList,
 					  exclusionOpNames, relationId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, isconstraint, istemporal, InvalidOid,
+					  amcanorder, isconstraint, isWithoutOverlaps, InvalidOid,
 					  0, NULL);
 
 	/* Get the soon-obsolete pg_index tuple. */
@@ -689,7 +689,7 @@ 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->istemporal;
+	exclusion = stmt->excludeOpNames || stmt->iswithoutoverlaps;
 
 	/* Ensure that it makes sense to index this kind of relation */
 	switch (rel->rd_rel->relkind)
@@ -859,7 +859,7 @@ DefineIndex(Oid tableId,
 	pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
 								 accessMethodId);
 
-	if (stmt->unique && !stmt->istemporal && !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",
@@ -927,7 +927,7 @@ DefineIndex(Oid tableId,
 					  coloptions, allIndexParams,
 					  stmt->excludeOpNames, tableId,
 					  accessMethodName, accessMethodId,
-					  amcanorder, stmt->isconstraint, stmt->istemporal,
+					  amcanorder, stmt->isconstraint, stmt->iswithoutoverlaps,
 					  root_save_userid, root_save_sec_context,
 					  &root_save_nestlevel);
 
@@ -955,8 +955,6 @@ DefineIndex(Oid tableId,
 
 		if (stmt->primary)
 			constraint_type = "PRIMARY KEY";
-		else if (stmt->unique && stmt->istemporal)
-			constraint_type = "temporal UNIQUE";
 		else if (stmt->unique)
 			constraint_type = "UNIQUE";
 		else if (stmt->excludeOpNames)
@@ -1006,7 +1004,7 @@ DefineIndex(Oid tableId,
 			 * 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 && !stmt->istemporal && 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\"",
@@ -1039,7 +1037,7 @@ DefineIndex(Oid tableId,
 					{
 						Oid			idx_eqop = InvalidOid;
 
-						if (stmt->unique && !stmt->istemporal)
+						if (stmt->unique && !stmt->iswithoutoverlaps)
 							idx_eqop = get_opfamily_member(idx_opfamily,
 														   idx_opcintype,
 														   idx_opcintype,
@@ -1197,8 +1195,8 @@ DefineIndex(Oid tableId,
 		constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
 	if (stmt->initdeferred)
 		constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
-	if (stmt->istemporal)
-		constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
+	if (stmt->iswithoutoverlaps)
+		constr_flags |= INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS;
 
 	indexRelationId =
 		index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1975,7 +1973,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
-				  bool istemporal,
+				  bool iswithoutoverlaps,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -2000,7 +1998,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 		nextExclOp = NULL;
 
 	/* exclusionOpNames can be non-NIL if we are creating a partition */
-	if (istemporal && exclusionOpNames == NIL)
+	if (iswithoutoverlaps && exclusionOpNames == NIL)
 	{
 		indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols);
 		indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols);
@@ -2283,7 +2281,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 			indexInfo->ii_ExclusionStrats[attn] = strat;
 			nextExclOp = lnext(exclusionOpNames, nextExclOp);
 		}
-		else if (istemporal)
+		else if (iswithoutoverlaps)
 		{
 			int strat;
 			Oid opid;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4cc645282a..04527a3540 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,7 +208,6 @@ typedef struct NewConstraint
 	Oid			refrelid;		/* PK rel, if FOREIGN */
 	Oid			refindid;		/* OID of PK's index, if FOREIGN */
 	Oid			conid;			/* OID of pg_constraint entry, if FOREIGN */
-	bool		contemporal;	/* Whether the new constraint is temporal */
 	Node	   *qual;			/* Check expr or CONSTR_FOREIGN Constraint */
 	ExprState  *qualstate;		/* Execution state for CHECK expr */
 } NewConstraint;
@@ -10094,7 +10093,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
-									  false,	/* conTemporal */
+									  false,	/* conWithoutOverlaps */
 									  false);	/* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10393,7 +10392,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conTemporal */
+									  false,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10899,7 +10898,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
-								  false,	/* conTemporal */
+								  false,	/* conWithoutOverlaps */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -11574,7 +11573,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 			newcon->refrelid = con->confrelid;
 			newcon->refindid = con->conindid;
 			newcon->conid = con->oid;
-			newcon->contemporal = con->contemporal;
 			newcon->qual = (Node *) fkconstraint;
 
 			/* Find or create work queue entry for this table */
@@ -11741,12 +11739,10 @@ 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.  If this is a temporal primary key,
- *	also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
+ *	index supporting the primary key.
  *
  *	All parameters except pkrel are output parameters.  Also, the function
- *	return value is the number of attributes in the primary key,
- *	not including the WITHOUT OVERLAPS if any.
+ *	return value is the number of attributes in the primary key.
  *
  *	Used when the column list in the REFERENCES specification is omitted.
  */
@@ -14292,7 +14288,7 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 							 stmt->accessMethod,
 							 stmt->indexParams,
 							 stmt->excludeOpNames,
-							 stmt->istemporal))
+							 stmt->iswithoutoverlaps))
 	{
 		Relation	irel = index_open(oldId, NoLock);
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0577b60415..a10a42b97f 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -841,7 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 											  true, /* islocal */
 											  0,	/* inhcount */
 											  true, /* noinherit */
-											  false, /* contemporal */
+											  false, /* conwithoutoverlaps */
 											  isInternal);	/* is_internal */
 	}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 1773556053..2470019818 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3544,7 +3544,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  true, /* is local */
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
-							  false,	/* contemporal */
+							  false,	/* conwithoutoverlaps */
 							  false);	/* is_internal */
 	if (constrAddr)
 		ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 14d4e24fcc..ff4add6225 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,13 +523,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableElement TypedTableElement ConstraintElem TableFuncElement
 %type <node>	columnDef columnOptions
 %type <defelt>	def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node>	def_arg columnElem without_overlaps_clause
-				where_clause where_or_current_clause
+%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>	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
@@ -4105,7 +4104,7 @@ ConstraintElem:
 					n->initially_valid = true;
 					$$ = (Node *) n;
 				}
-			| UNIQUE opt_unique_null_treatment '(' columnList without_overlaps_clause ')' 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);
@@ -4140,7 +4139,7 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
-			| PRIMARY KEY '(' columnList without_overlaps_clause ')' opt_c_include opt_definition OptConsTableSpace
+			| PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
 				ConstraintAttributeSpec
 				{
 					Constraint *n = makeNode(Constraint);
@@ -4221,6 +4220,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 			| /* EMPTY */							{  $$ = false; }
 		;
 
+opt_without_overlaps:
+			WITHOUT OVERLAPS						{ $$ = true; }
+			| /*EMPTY*/								{ $$ = false; }
+	;
+
 opt_column_list:
 			'(' columnList ')'						{ $$ = $2; }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -4231,11 +4235,6 @@ columnList:
 			| columnList ',' columnElem				{ $$ = lappend($1, $3); }
 		;
 
-without_overlaps_clause:
-			',' columnElem WITHOUT OVERLAPS { $$ = $2; }
-			| /*EMPTY*/               { $$ = NULL; }
-	;
-
 columnElem: ColId
 				{
 					$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f4705ecc2b..547e3058b9 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,7 +124,7 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
 static void transformIndexConstraints(CreateStmtContext *cxt);
 static IndexStmt *transformIndexConstraint(Constraint *constraint,
 										   CreateStmtContext *cxt);
-static void validateWithoutOverlaps(CreateStmtContext *cxt, char *colname, int location);
+static void validateWithoutOverlaps(CreateStmtContext *cxt, const char *colname, int location);
 static void transformExtendedStatistics(CreateStmtContext *cxt);
 static void transformFKConstraints(CreateStmtContext *cxt,
 								   bool skipValidation,
@@ -1717,7 +1717,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 	index->unique = idxrec->indisunique;
 	index->nulls_not_distinct = idxrec->indnullsnotdistinct;
 	index->primary = idxrec->indisprimary;
-	index->istemporal = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
+	index->iswithoutoverlaps = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
 	index->transformed = true;	/* don't need transformIndexStmt */
 	index->concurrent = false;
 	index->if_not_exists = false;
@@ -1768,7 +1768,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			i;
 
 				Assert(conrec->contype == CONSTRAINT_EXCLUSION ||
-						(index->istemporal &&
+						(index->iswithoutoverlaps &&
 						 (conrec->contype == CONSTRAINT_PRIMARY || conrec->contype == CONSTRAINT_UNIQUE)));
 				/* Extract operator OIDs from the pg_constraint tuple */
 				datum = SysCacheGetAttrNotNull(CONSTROID, ht_constr,
@@ -2309,7 +2309,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 	}
 	index->nulls_not_distinct = constraint->nulls_not_distinct;
 	index->isconstraint = true;
-	index->istemporal = constraint->without_overlaps != NULL;
+	index->iswithoutoverlaps = constraint->without_overlaps;
 	index->deferrable = constraint->deferrable;
 	index->initdeferred = constraint->initdeferred;
 
@@ -2555,6 +2555,10 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 			ListCell   *columns;
 			IndexElem  *iparam;
 
+			/* handled below */
+			if (constraint->without_overlaps && lc == list_last_cell(constraint->keys))
+				break;
+
 			/* Make sure referenced column exists. */
 			foreach(columns, cxt->columns)
 			{
@@ -2688,11 +2692,21 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 		 * Anything in without_overlaps should be included,
 		 * but with the overlaps operator (&&) instead of equality.
 		 */
-		if (constraint->without_overlaps != NULL)
+		if (constraint->without_overlaps)
 		{
-			char *without_overlaps_str = strVal(constraint->without_overlaps);
+			char *without_overlaps_str = strVal(llast(constraint->keys));
 			IndexElem *iparam = makeNode(IndexElem);
 
+			/*
+			 * This enforces that there is at last 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"));
+
 			/*
 			 * Iterate through the table's columns
 			 * (like just a little bit above).
@@ -2855,7 +2869,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
  * and the new ones. Raises an error if we can't find one or it's not a range type.
  */
 static void
-validateWithoutOverlaps(CreateStmtContext *cxt, char *colname, int location)
+validateWithoutOverlaps(CreateStmtContext *cxt, const char *colname, int location)
 {
 	/* Check the new columns first in case their type is changing. */
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f2da27eb50..25ac0ad1bc 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,7 +338,7 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext,
 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, Oid indexId,
+static int	decompile_column_index_array(Datum column_index_array, Oid relId,
 										 bool withoutOverlaps, StringInfo buf);
 static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
 static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
@@ -2247,7 +2247,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
-				decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
+				decompile_column_index_array(val, conForm->conrelid, false, &buf);

-				decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
+				decompile_column_index_array(val, conForm->confrelid, false, &buf);

+					decompile_column_index_array(val, conForm->conrelid, false, &buf);
 
@@ -2357,7 +2357,6 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				Oid			indexId;
 				int			keyatts;
 				HeapTuple	indtup;
-				bool		isnull;
 
@@ -2380,14 +2379,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 
-				/*
-				 * If it has exclusion-style operator OIDs
-				 * then it uses WITHOUT OVERLAPS.
-				 */
-				indexId = conForm->conindid;
-				SysCacheGetAttr(CONSTROID, tup,
-						  Anum_pg_constraint_conexclop, &isnull);
-				keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
+				keyatts = decompile_column_index_array(val, conForm->conrelid, conForm->conwithoutoverlaps, &buf);

@@ -2582,7 +2574,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  * of keys.
  */
 static int
-decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+decompile_column_index_array(Datum column_index_array, Oid relId,
 							 bool withoutOverlaps, StringInfo buf)
 {
 	Datum	   *keys;
@@ -2596,17 +2588,16 @@ decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
 	for (j = 0; j < nKeys; j++)
 	{
 		char	   *colName;
-		int			colid = DatumGetInt16(keys[j]);
 
-		colName = get_attname(relId, colid, false);
+		colName = get_attname(relId, DatumGetInt16(keys[j]), false);
 
 		if (j == 0)
 			appendStringInfoString(buf, quote_identifier(colName));
-		else if (withoutOverlaps && j == nKeys - 1)
-			appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
 		else
 			appendStringInfo(buf, ", %s", quote_identifier(colName));
 	}
+	if (withoutOverlaps)
+		appendStringInfoString(buf, " WITHOUT OVERLAPS");
 
 	return nKeys;
 }
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 45810ac35f..f341c5c370 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5568,12 +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 or temporal primary key/unique
- * 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,
@@ -5638,7 +5640,7 @@ RelationGetExclusionInfo(Relation indexRelation,
 
 		/* We want the exclusion constraint owning the index */
 		if ((conform->contype != CONSTRAINT_EXCLUSION &&
-					!(conform->contemporal && (
+					!(conform->conwithoutoverlaps && (
 							conform->contype == CONSTRAINT_PRIMARY
 							|| conform->contype == CONSTRAINT_UNIQUE))) ||
 			conform->conindid != RelationGetRelid(indexRelation))
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 54a15cf64a..c89b6da8b2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6983,14 +6983,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conwithoutoverlaps,
 				i_contableoid,
 				i_conoid,
 				i_condef,
 				i_tablespace,
 				i_indreloptions,
 				i_indstatcols,
-				i_indstatvals,
-				i_withoutoverlaps;
+				i_indstatvals;
 
 	/*
 	 * We want to perform just one query against pg_index.  However, we
@@ -7072,10 +7072,10 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	if (fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query,
-							 "c.conexclop IS NOT NULL AS withoutoverlaps ");
+							 "c.conwithoutoverlaps ");
 	else
 		appendPQExpBufferStr(query,
-							 "null AS withoutoverlaps ");
+							 "NULL AS conwithoutoverlaps ");
 
 	/*
 	 * The point of the messy-looking outer join is to find a constraint that
@@ -7143,6 +7143,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");
@@ -7150,7 +7151,6 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 	i_indreloptions = PQfnumber(res, "indreloptions");
 	i_indstatcols = PQfnumber(res, "indstatcols");
 	i_indstatvals = PQfnumber(res, "indstatvals");
-	i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");

@@ -7251,9 +7251,9 @@ 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;
-				constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
 			}
@@ -16949,23 +16949,12 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 					break;
 				attname = getAttrName(indkey, tbinfo);
 
-				if (k == 0)
-				{
-					appendPQExpBuffer(q, "%s",
-										fmtId(attname));
-				}
-				else if (k == indxinfo->indnkeyattrs - 1 &&
-						coninfo->withoutoverlaps)
-				{
-					appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
-									  fmtId(attname));
-				}
-				else
-				{
-					appendPQExpBuffer(q, ", %s",
-										fmtId(attname));
-				}
+				appendPQExpBuffer(q, "%s%s",
+								  (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 ad7cc85b8c..cbf4b5e179 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -487,9 +487,9 @@ 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 */
-	bool		withoutoverlaps;	/* true if the last elem is WITHOUT OVERLAPS */
 } ConstraintInfo;
 
index eb35d5ef60..8584a4b406 100644
@@ -1015,12 +1015,11 @@
 		},
 	},
 
-	'ALTER TABLE ONLY test_table_tpk ADD CONSTRAINT ... PRIMARY KEY (..., ... WITHOUT OVERLAPS)' => {
+	'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));',
+							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
@@ -1029,21 +1028,18 @@
 			%full_runs,
 			%dump_test_schema_runs,
 			section_post_data  => 1,
-			exclude_test_table => 1,
 		},
 		unlike => {
-			only_dump_test_table     => 1,
 			exclude_dump_test_schema => 1,
 			only_dump_measurement    => 1,
 		},
 	},
 
-	'ALTER TABLE ONLY test_table_tuq ADD CONSTRAINT ... UNIQUE (..., ... WITHOUT OVERLAPS)' => {
+	'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));',
+							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
@@ -1052,10 +1048,8 @@
 			%full_runs,
 			%dump_test_schema_runs,
 			section_post_data  => 1,
-			exclude_test_table => 1,
 		},
 		unlike => {
-			only_dump_test_table     => 1,
 			exclude_dump_test_schema => 1,
 			only_dump_measurement    => 1,
 		},
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4fbfc07460..135585411a 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2384,10 +2384,10 @@ describeOneTableDetails(const char *schemaname,
 			else
 				appendPQExpBufferStr(&buf, ", false AS indisreplident");
 			appendPQExpBufferStr(&buf, ", c2.reltablespace");
-			if (pset.sversion >= 160000)
-				appendPQExpBufferStr(&buf, ", con.contemporal");
+			if (pset.sversion >= 170000)
+				appendPQExpBufferStr(&buf, ", con.conwithoutoverlaps");
 			else
-				appendPQExpBufferStr(&buf, ", false AS contemporal");
+				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"
@@ -2410,8 +2410,8 @@ describeOneTableDetails(const char *schemaname,
 									  PQgetvalue(result, i, 0));
 
 					/*
-					 * If exclusion constraint or temporal PK/UNIQUE constraint,
-					 * print the constraintdef
+					 * 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)
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index 1a858c3897..0fccff9c05 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -92,7 +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_TEMPORAL		(1 << 5)
+#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 98a560741d..c17b1e9d31 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -108,10 +108,10 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	bool		connoinherit;
 
 	/*
-	 * For primary and foreign keys, signifies the last column is a range
-	 * and should use overlaps instead of equals.
+	 * For primary keys and unique constraints, signifies the last column uses
+	 * overlaps instead of equals.
 	 */
-	bool		contemporal;
+	bool		conwithoutoverlaps;

@@ -152,7 +152,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 
 	/*
 	 * If an exclusion constraint, the OIDs of the exclusion operators for
-	 * each column of the constraint. Also set for temporal primary keys.
+	 * each column of the constraint.  Also set for unique constraints/primary
+	 * keys using WITHOUT OVERLAPS.
 	 */
 	Oid			conexclop[1] BKI_LOOKUP(pg_operator);
 
@@ -242,7 +243,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
-								  bool conTemporal,
+								  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 f10886f6ca..25f55c181b 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -45,7 +45,7 @@ extern bool CheckIndexCompatible(Oid oldId,
 								 const char *accessMethodName,
 								 const List *attributeList,
 								 const List *exclusionOpNames,
-								 bool istemporal);
+								 bool isWithoutOverlaps);
 extern Oid	GetDefaultOpClass(Oid type_id, Oid am_id);
 extern Oid	ResolveOpClass(const List *opclass, Oid attrType,
 						   const char *accessMethodName, Oid accessMethodId);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ff65db2db1..098990dd16 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2589,6 +2589,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) */
 
@@ -2617,9 +2618,6 @@ typedef struct Constraint
 	Oid			old_pktable_oid;	/* pg_constraint.confrelid of my former
 									 * self */
 
-	/* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
-	Node	   *without_overlaps; /* String node naming range column */
-
 	/* Fields used for constraints that allow a NOT VALID specification */
 	bool		skip_validation;	/* skip validation of existing rows? */
 	bool		initially_valid;	/* mark the new constraint as valid? */
@@ -3221,7 +3219,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		istemporal;		/* is it for a temporal pkey? */
+	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
index 36aebc452f..e565490a38 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -11,9 +11,7 @@ CREATE TABLE temporal_rng (
 	valid_at tsrange,
 	CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
 );
-ERROR:  syntax error at or near "WITHOUT"
-LINE 3:  CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OV...
-                                                          ^
+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,
@@ -106,9 +104,7 @@ CREATE TABLE temporal_rng3 (
 	valid_at tsrange,
 	CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS)
 );
-ERROR:  syntax error at or near "WITHOUT"
-LINE 3:  CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLA...
-                                                      ^
+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,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16683148d2..b08aaae44b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
 
 # ----------
 # Load huge amounts of data
@@ -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
-- 
2.42.0

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 9 Nov 2023 09:15:31 -0800

On 11/9/23 05:47, Peter Eisentraut wrote:
> detail

Thanks Peter!

I'm about halfway through jian he's last two emails. I'll address your 

> just need to make sure we have all the weird corner cases covered.

One remaining issue I know about is with table partitions whose column 
order has changed. I've got an in-progress fix for that, but I've been 
prioritizing reviewer feedback the last few months. Just want to make 
sure you know about it for now.

Thanks!

---

From: jian he <[email-masked]>
Date: Fri, 17 Nov 2023 15:12:30 +0800

based on v17.

begin;
drop table if exists s1;
CREATE TABLE s1 (id numrange, misc int, misc1 text);
create role  test101 login;
grant update, select  on s1 to test101;
insert into s1 VALUES ('[1,1000]',2);
set session authorization test101;
update s1 set id = '[1,1000]';
savepoint sp1;
update s1 FOR PORTION OF id from 10 to 100 set misc1 = 'test';
table s1;
savepoint sp2;
insert into s1 VALUES ('[2,1000]',12);
rollback;

In UPDATE FOR PORTION OF from x to y, if range [x,y) overlaps with the
"source" range
then the UPDATE action would be UPDATE and INSERT.
The above UPDATE FOR PORTION OF query should fail?
UPDATE FOR PORTION OF, may need insert privilege. We also need to document this.
Similarly, we also need to apply the above logic to DELETE FOR PORTION OF.
-------------------------------------------------------
+  <para>
+   If the table has a <link
linkend="ddl-periods-application-periods">range column
+   or <literal>PERIOD</literal></link>, you may supply a

should be

+ <para>
+  If the table has a range column or  <link
linkend="ddl-periods-application-periods">
+  <literal>PERIOD</literal></link>, you may supply a

similarly the doc/src/sgml/ref/delete.sgml the link reference also broken.
--------------------------------------------------------
  <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 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 it will be
truncated to fit
   within the bounds, and new rows spanning the "cut off" duration will be
   inserted to preserve the old values.
  </para>

 "given interval", "cut off" these words,  imho, feel not so clear.
We also need a document that:
 "UPDATE FOR PORTION OF" is UPDATE and INSERT (if overlaps).
If the "UPDATE FOR PORTION OF" range overlaps then
It will invoke triggers in the following order: before row update,
before row insert, after row insert. after row update.
---------------------------------------
src/test/regress/sql/for_portion_of.sql
You only need to create two triggers?
since for_portion_of_trigger only raises notice to output the triggers
meta info.

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();

---

From: Paul Jungwirth <[email-masked]>
Date: Fri, 17 Nov 2023 10:39:58 -0800

On 11/9/23 05:47, Peter Eisentraut wrote:
> I went over the patch v17-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch in more 
> detail.  Attached is a fixup patch that addresses a variety of cosmetic issues.

Thanks for the review! This all looks great to me, and it's applied in the attached patch (with one 
typo correction in a C comment). The patch addresses some of jian he's feedback too but I'll reply 
to those emails separately.

> 1) In src/backend/commands/indexcmds.c, get_index_attr_temporal_operator() has this comment:
> 
> This doesn't see very confident. ;-)  I don't quite understand this.  Is this a gap in the currently 
> available APIs, do we need to improve something here, or does this need more research?

I've improved this a bit but I'm still concerned about part of it.

First the improved part: I realized I should be calling get_opclass_opfamily_and_input_type first 
and passing the opcintype to get_opfamily_member, which solves the problem of having a concrete 
rangetype but needing an operator that targets anyrange. We do the same thing with partition keys.

But I feel the overall approach is wrong: originally I used hardcoded "=" and "&&" operators, and 
you asked me to look them up by strategy number instead. But that leads to trouble with core gist 
types vs btree_gist types. The core gist opclasses use RT*StrategyNumbers, but btree_gist creates 
opclasses with BT*StrategyNumbers. I don't see any way to ask ahead of time which class of strategy 
numbers are used by a given opclass. So I have code like this:

     *strat = RTEqualStrategyNumber;
     opname = "equality";
     *opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);

     /*
      * For the non-overlaps key elements,
      * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
      * If you're using btree_gist then you'll need the latter.
      */
     if (!OidIsValid(*opid))
     {
         *strat = BTEqualStrategyNumber;
         *opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
     }

I do a similar thing for foreign keys.

But that can't be right. I added a scary comment there in this patch, but I'll explain here too:

It's only by luck that RTEqualStrategyNumber (18) is bigger than any BT*StrategyNumber. If I checked 
in the reverse order, I would always find an operator---it would just sometimes be the wrong one! 
And what if someone has defined a new type+opclass with totally different strategy numbers? As far 
as I can tell, the gist AM doesn't require an opclass have any particular operators, only support 
functions, so the strategy numbers are "private" and can vary between opclasses.

What we want is a way to ask which operators mean equality & overlaps for a given opclass. But the 
strategy numbers aren't meaningful terms to ask the question.

So I think asking for "=" and "&&" is actually better here. Those will be correct for both core & 
btree_gist, and they should also match user expectations for custom types. They are what you'd use 
in a roll-your-own temporal constraint via EXCLUDE. We can also document that we implement WITHOUT 
OVERLAPS with those operator names, so people can get the right behavior from custom types.

(This also maybe lets us implement WITHOUT OVERLAPS for more than rangetypes, as you suggested. See 
below for more about that.)

It's taken me a while to grok the am/opclass/opfamily/amop interaction, and maybe I'm still missing 
something here. Let me know if that's the case!

> 2) In src/backend/parser/parse_utilcmd.c, transformIndexConstraint(), there is too much duplication 
> between the normal and the if (constraint->without_overlaps) case, like the whole not-null 
> constraints stuff at the end.  This should be one code block with a few conditionals inside.  Also, 
> the normal case deals with things like table inheritance, which the added parts do not.  Is this all 
> complete?

Cleaned things up here. I agree it's much better now.

And you're right, now you should be able to use an inherited column in a temporal PK/UQ constraint. 
I think I need a lot more test coverage for how this feature combines with inherited tables, so I'll 
work on that.

> I'm not sure the validateWithoutOverlaps() function is needed at this point in the code.

Agreed, I removed it and moved the is-it-a-rangetype check into the caller.

> We don't even need to 
> restrict this to range types.  Consider for example, it's possible that a type does not have a btree 
> equality operator.  We don't check that here either, but let the index code later check it.

That is very interesting. Perhaps we allow anything with equals and overlaps then?

Note that we need more for FOR PORTION OF, foreign keys, and foreign keys with CASCADE/SET. So it 
might be confusing if a type works with temporal PKs but not those other things. But if we 
documented what operators you need for each feature then you could implement as much as you liked.

I like this direction a lot. It matches what I suggested in the conversation about multiple WITHOUT 
OVERLAPS/PERIOD columns: rather than having foreign keys and FOR PORTION OF know how to find 
n-dimensional "leftovers" we could leave it up the type, and just call a documented operator. (We 
would need to add that operator for rangetypes btw, one that calls range_leftover_internal. It 
should return an array (not a multirange!) of the untouched parts of the record.) This makes it easy 
to support bi/tri/n-temporal, spatial, multiranges, etc.

(For spatial you probably want PostGIS instead, and I'm wary of over-abstracting here, but I like 
how this "leaves the door open" for PostGIS to eventually support spatial PKs/FKs.)

Please let me know what you think!

Yours,

From 107f0e22679e41ac29ca1784ea96e9ff57cf8691 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v18 1/5] 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                   |   3 +-
 .../btree_gist/expected/without_overlaps.out  |  31 ++
 contrib/btree_gist/sql/without_overlaps.sql   |  13 +
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/ref/create_table.sgml            |  39 +-
 src/backend/catalog/heap.c                    |   2 +
 src/backend/catalog/index.c                   |   4 +
 src/backend/catalog/pg_constraint.c           |   2 +
 src/backend/commands/indexcmds.c              | 159 +++++++-
 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            |  67 ++-
 src/backend/utils/adt/ruleutils.c             |  14 +-
 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/catalog/index.h                   |   1 +
 src/include/catalog/pg_constraint.h           |  10 +-
 src/include/commands/defrem.h                 |   3 +-
 src/include/nodes/parsenodes.h                |   2 +
 .../regress/expected/without_overlaps.out     | 382 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/without_overlaps.sql     | 282 +++++++++++++
 29 files changed, 1099 insertions(+), 55 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 073dcc745c..6dd05d6f50 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -38,7 +38,8 @@ 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 \
+        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 0000000000..069272688f
--- /dev/null
+++ b/contrib/btree_gist/expected/without_overlaps.out
@@ -0,0 +1,31 @@
+-- Core must test WITHOUT OVERLAPS
+-- with an int4range + tsrange,
+-- so here we do some simple tests
+-- to make sure int + tsrange works too,
+-- since that is the expected use-case.
+CREATE TABLE temporal_rng (
+  id integer,
+  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       | integer |           | 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)
+
diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql
new file mode 100644
index 0000000000..98665dbb9e
--- /dev/null
+++ b/contrib/btree_gist/sql/without_overlaps.sql
@@ -0,0 +1,13 @@
+-- Core must test WITHOUT OVERLAPS
+-- with an int4range + tsrange,
+-- so here we do some simple tests
+-- to make sure int + tsrange works too,
+-- since that is the expected use-case.
+CREATE TABLE temporal_rng (
+  id integer,
+  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';
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3ec7391ec5..618a05ac89 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>
index e04a0692c4..a0bd2b8adc 100644
@@ -965,7 +965,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM

     <listitem>
@@ -979,6 +979,26 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       of these columns.
      </para>
 
+     <para>
@@ -1001,7 +1021,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
      <para>
 
      <para>
@@ -1019,7 +1041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1052,9 +1074,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
 
      <para>
index 7224d96695..6b6609e23a 100644
 
 	pfree(ccbin);
index 143fae01eb..8444d768e5 100644

 
 	/*
index e9d4d6006e..ffd340a2e4 100644
 
 	if (conkeyArray)
index 0b3b8e98b8..1298d917d8 100644

 
+	/*

 	/*
@@ -985,10 +1001,10 @@ DefineIndex(Oid tableId,
@@ -1021,12 +1037,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -1035,7 +1051,7 @@ DefineIndex(Oid tableId,
 							found = true;
 							break;
 						}
-						else if (stmt->excludeOpNames)
+						else if (exclusion)
 						{
 							/*
 							 * We found a match, but it's not an equality
@@ -1179,6 +1195,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,
@@ -1820,6 +1838,97 @@ CheckPredicate(Expr *predicate)
 				 errmsg("functions in index predicate must be marked IMMUTABLE")));
 }
 
+/*
+ * get_index_attr_temporal_operator
+ *
+ * Finds an operator for a temporal index attribute.
+ * We need an equality operator for normal keys
+ * and an overlaps operator for the range.
+ * Returns the operator oid and strategy in opid and strat,
+ * respectively.
+ */
+static void
+get_index_attr_temporal_operator(Oid opclass,
+								 Oid atttype,
+								 bool isoverlaps,
+								 Oid *opid,
+								 int *strat)
+{
+	Oid opfamily;
+	Oid opcintype;
+	const char *opname;
+
+	*opid = InvalidOid;
+
+	if (get_opclass_opfamily_and_input_type(opclass,
+											&opfamily,
+											&opcintype))
+	{
+		if (isoverlaps)
+		{
+			*strat = RTOverlapStrategyNumber;
+			opname = "overlaps";
+			*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
+		}
+		else
+		{
+			*strat = RTEqualStrategyNumber;
+			opname = "equality";
+			*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
+
+			/*
+			 * For the non-overlaps key elements,
+			 * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
+			 * If you're using btree_gist then you'll need the latter.
+			 *
+			 * TODO: This is scary
+			 * because what if there is ever an RTSomethingStrategyNumber
+			 * that happens to equal BTEqualStrategyNumber,
+			 * and we actually find an opid above,
+			 * but not one for equality?
+			 *
+			 * If we tried this lookup in reverse order (BTEqual first, then RTEqual),
+			 * that coincidence would happen, and we'd get weird results.
+			 *
+			 * So I guess what we need is a way for an opclass to tell us
+			 * what method's strategy numbers it uses,
+			 * or at least what is its strategy number for equality?
+			 */
+			if (!OidIsValid(*opid))
+			{
+				*strat = BTEqualStrategyNumber;
+				*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))));
+	}
+}
+
 /*
  * Compute per-index-column information, including indexed column numbers
  * or index expressions, opclasses and their options. Note, all output vectors
@@ -1843,6 +1952,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				  Oid accessMethodId,
 				  bool amcanorder,
 				  bool isconstraint,
+				  bool iswithoutoverlaps,
 				  Oid ddl_userid,
 				  int ddl_sec_context,
 				  int *ddl_save_nestlevel)
@@ -1866,6 +1976,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;

@@ -2142,6 +2260,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
+											 attn == nkeycols - 1,
 
 		/*
index 323d9bf870..bbf4d6f126 100644
@@ -10093,6 +10093,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10391,6 +10392,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10896,6 +10898,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conWithoutOverlaps */
 								  true);
 
@@ -14284,7 +14287,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 
index 52177759ab..a10a42b97f 100644
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index aaf9728697..2470019818 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 e66a99247e..f52ae3fc97 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 cc2021c1f7..b6ef30ce0d 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 c224df4ecc..ff4add6225 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -528,7 +528,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
@@ -4104,7 +4104,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);
@@ -4113,11 +4113,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;
@@ -4138,7 +4139,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);
@@ -4146,11 +4147,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;
@@ -4218,6 +4220,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 			| /* EMPTY */							{  $$ = false; }
 		;
 
+opt_without_overlaps:
index cf0d432ab1..6030ba90cd 100644
@@ -1716,6 +1716,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
@@ -1765,7 +1766,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
 				int			nElems;
 				int			i;
 
@@ -2305,6 +2308,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 
@@ -2397,6 +2401,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 					 errmsg("index \"%s\" is not valid", index_name),
 					 parser_errposition(cxt->pstate, constraint->location)));
 
+		/*
@@ -2652,6 +2661,45 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 				}
 			}
 
+			/* If it's WITHOUT OVERLAPS, it must be a range type */
+			if (constraint->without_overlaps && lc == list_last_cell(constraint->keys))
+			{
+				Oid	typid = InvalidOid;
+
+				if (!found && cxt->isalter)
+				{
+					/*
+					 * Look up 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;
+						}
+					}
+				}
+				else
+					typid = typenameTypeId(NULL, column->typeName);
+
+				if (OidIsValid(typid) && !type_is_range(typid))
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type",
+									key),
+							 parser_errposition(cxt->pstate, constraint->location)));
+			}
+
 			/* OK, add it to the index definition */
 			iparam = makeNode(IndexElem);
 			iparam->name = pstrdup(key);
@@ -2673,6 +2721,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";
+		}
+
 	}
 
 	/*
index ed7f40f053..25ac0ad1bc 100644
 static int	decompile_column_index_array(Datum column_index_array, Oid relId,

 

 
@@ -2379,7 +2379,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);

 
@@ -2575,7 +2575,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  */
 static int
 decompile_column_index_array(Datum column_index_array, Oid relId,
-							 StringInfo buf)
+							 bool withoutOverlaps, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2596,6 +2596,8 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
 		else
 			appendStringInfo(buf, ", %s", quote_identifier(colName));
 	}
+	if (withoutOverlaps)
+		appendStringInfoString(buf, " WITHOUT OVERLAPS");
 
 	return nKeys;
 }
index b3faccbefe..f341c5c370 100644
@@ -5568,11 +5568,14 @@ RelationGetIdentityKeyBitmap(Relation relation)
@@ -5636,7 +5639,10 @@ RelationGetExclusionInfo(Relation indexRelation,
 		int			nelem;

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 34fd0a86e9..1709007dc6 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6985,6 +6985,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conwithoutoverlaps,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7066,10 +7067,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7137,6 +7145,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -16945,6 +16955,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 2fe3cbed9a..cbf4b5e179 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -487,6 +487,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 eb3ec534b4..8584a4b406 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 = (
 		},
 	},
 
index 5077e7b358..61d6452b53 100644
@@ -2379,6 +2379,10 @@ describeOneTableDetails(const char *schemaname,
@@ -2400,8 +2404,12 @@ describeOneTableDetails(const char *schemaname,
 
index a4770eaf12..0fccff9c05 100644
 
index a026b42515..c17b1e9d31 100644
 
+	/*
 
 	/*
@@ -146,7 +152,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 
 	/*
 
@@ -236,6 +243,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
+								  bool conWithoutOverlaps,
 								  bool is_internal);
 
index 179eb9901f..25f55c181b 100644
index e494309da8..098990dd16 100644
 
@@ -3218,6 +3219,7 @@ typedef struct IndexStmt
index 0000000000..c7441379c1
@@ -0,0 +1,382 @@
index f0987ff537..b08aaae44b 100644
 
index 0000000000..701bcfb962

From 5d93a35eb8f84bb28919a220ca3e323cc1fdaf66 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v18 2/5] 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  |  27 +
 contrib/btree_gist/sql/without_overlaps.sql   |  12 +
 doc/src/sgml/ref/create_table.sgml            |  48 +-
 src/backend/commands/tablecmds.c              | 791 ++++++++++++------
 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           | 342 +++++++-
 src/backend/utils/adt/ruleutils.c             |  19 +-
 src/include/catalog/pg_constraint.h           |   8 +-
 src/include/catalog/pg_proc.dat               |  24 +
 src/include/nodes/parsenodes.h                |   2 +
 src/include/parser/kwlist.h                   |   1 +
 .../regress/expected/without_overlaps.out     | 551 ++++++++++++
 src/test/regress/sql/without_overlaps.sql     | 526 ++++++++++++
 15 files changed, 2048 insertions(+), 344 deletions(-)

diff --git a/contrib/btree_gist/expected/without_overlaps.out b/contrib/btree_gist/expected/without_overlaps.out
index 069272688f..d11601e570 100644
--- a/contrib/btree_gist/expected/without_overlaps.out
+++ b/contrib/btree_gist/expected/without_overlaps.out
@@ -29,3 +29,30 @@ SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'te
  CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at)
 (1 row)
 
+-- Foreign key
+CREATE TABLE temporal_fk_rng2rng (
+  id integer,
+  valid_at tsrange,
+  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  | tsrange |           | 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)
+
diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql
index 98665dbb9e..036ee0a760 100644
--- a/contrib/btree_gist/sql/without_overlaps.sql
+++ b/contrib/btree_gist/sql/without_overlaps.sql
@@ -11,3 +11,15 @@ CREATE TABLE temporal_rng (
 \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';
+
+-- Foreign key
+CREATE TABLE temporal_fk_rng2rng (
+  id integer,
+  valid_at tsrange,
+  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';
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index a0bd2b8adc..b5f48525de 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 ]
@@ -1146,8 +1146,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>
 
@@ -1158,11 +1158,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1236,6 +1254,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1247,6 +1271,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1260,6 +1291,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
index bbf4d6f126..4cd4ab9dd0 100644
@@ -207,6 +207,7 @@ typedef struct NewConstraint
 	ConstrType	contype;		/* CHECK or FOREIGN */
+	bool		conwithperiod;	/* Whether the new FOREIGN KEY uses PERIOD */
@@ -379,17 +380,20 @@ static int	transformColumnNameList(Oid relId, List *colList,
@@ -499,7 +503,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -509,7 +514,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -541,6 +548,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
@@ -5898,7 +5911,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 
+											 con->conwithperiod);
 
 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9527,6 +9541,11 @@ 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 = (fkconstraint->fk_period != NULL);
+	int16		pkperiodattnums[1] = {0};
+	int16		fkperiodattnums[1] = {0};
+	Oid			pkperiodtypoids[1] = {0};
+	Oid			fkperiodtypoids[1] = {0};
 	int			i;
 	int			numfks,
 				numpks,
@@ -9621,6 +9640,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfks = transformColumnNameList(RelationGetRelid(rel),
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
+	if (is_temporal)
+		transformColumnNameList(RelationGetRelid(rel),
+							  list_make1(fkconstraint->fk_period),
+							  fkperiodattnums, fkperiodtypoids);
+	else
+		if (fkconstraint->pk_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,
@@ -9639,7 +9667,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
 											&fkconstraint->pk_attrs,
+											&fkconstraint->pk_period,
 											pkattnum, pktypoid,
+											pkperiodattnums, pkperiodtypoids,
 											opclasses);
 	}
 	else
@@ -9647,8 +9677,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		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),
+									pkperiodattnums, pkperiodtypoids);
+		}
+
 		/* Look for an index matching the column list */
 		indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+										   is_temporal, pkperiodattnums[0],
 										   opclasses);
 	}
 
@@ -9707,187 +9751,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
 	/*
@@ -9906,7 +9790,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
@@ -9922,7 +9807,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
 	 * Done.  Close pk table, but keep lock until we've committed.
@@ -10007,7 +9893,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;
@@ -10093,7 +9980,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);	/* is_internal */
 
@@ -10169,7 +10056,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			/* Done -- clean up (but keep the lock) */
 			table_close(partRel, NoLock);
@@ -10227,7 +10115,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;
@@ -10275,6 +10164,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);
@@ -10392,7 +10282,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10423,7 +10313,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10659,7 +10550,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10898,7 +10790,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
-								  false,	/* conWithoutOverlaps */
+								  constrForm->conwithoutoverlaps,	/* conWithoutOverlaps */
 								  true);
 
 		/* Set up partition dependencies for the new constraint */
@@ -10932,13 +10824,245 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								constrForm->conwithoutoverlaps);
 		table_close(pkrel, NoLock);
 	}

+static void
@@ -11436,7 +11560,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11683,7 +11811,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11749,7 +11876,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;
@@ -11814,36 +11943,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11870,6 +12018,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11891,12 +12043,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11934,6 +12090,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12043,7 +12212,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12072,8 +12242,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;
 
 	/*
@@ -12142,6 +12314,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12161,12 +12334,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12224,37 +12403,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12284,37 +12494,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index f52ae3fc97..3bdd46a413 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 b6ef30ce0d..190ec14ef1 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 ff4add6225..9a6f3f647a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -521,12 +521,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
@@ -744,7 +745,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER

@@ -4193,21 +4194,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
@@ -4235,6 +4238,16 @@ columnList:
 
+optionalPeriodName:
@@ -17558,6 +17571,7 @@ reserved_keyword:
@@ -17867,6 +17881,7 @@ bare_label_keyword:
index 6945d99b3d..d00c6e5334 100644

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

 

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

 

+	riinfo->temporal = conForm->conwithoutoverlaps;

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

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

index 25ac0ad1bc..1bf3aeb342 100644
 static int	decompile_column_index_array(Datum column_index_array, Oid relId,
@@ -2247,7 +2247,9 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 
-				decompile_column_index_array(val, conForm->conrelid, false, &buf);
+				/* If it is a temporal foreign key then it uses PERIOD. */
+				decompile_column_index_array(val, conForm->conrelid, false,
+											 conForm->conwithoutoverlaps, &buf);
 
@@ -2258,7 +2260,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 
-				decompile_column_index_array(val, conForm->confrelid, false, &buf);
+				decompile_column_index_array(val, conForm->confrelid, false,
+											 conForm->conwithoutoverlaps, &buf);

@@ -2344,7 +2347,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (!isnull)
 				{
 					appendStringInfoString(&buf, " (");
-					decompile_column_index_array(val, conForm->conrelid, false, &buf);
+					decompile_column_index_array(val, conForm->conrelid, false, 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, conForm->conwithoutoverlaps, &buf);
+				keyatts = decompile_column_index_array(val, conForm->conrelid, conForm->conwithoutoverlaps, false, &buf);

@@ -2575,7 +2578,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
  */
 static int
 decompile_column_index_array(Datum column_index_array, Oid relId,
-							 bool withoutOverlaps, StringInfo buf)
+							 bool withoutOverlaps, bool withPeriod, StringInfo buf)
 {
 	Datum	   *keys;
 	int			nKeys;
@@ -2594,7 +2597,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));
 	}
 	if (withoutOverlaps)
 		appendStringInfoString(buf, " WITHOUT OVERLAPS");
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index c17b1e9d31..0cc15a50d7 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];
 
 	/*
 
 	/*
 
 	/*
@@ -181,7 +181,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, pg

 
index fb58dee3bc..e70b391e04 100644
 
index 098990dd16..adfc5a223c 100644
@@ -2609,7 +2609,9 @@ typedef struct Constraint
index c7441379c1..12d2ebd6a5 100644
@@ -380,3 +380,554 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, 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)
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+		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)
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)
+ERROR:  foreign key uses PERIOD on the referenced table but not the referencing table
index 701bcfb962..21241ef9c2 100644
@@ -280,3 +280,529 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+-- with mismatched PERIOD columns:
+-- (parent_id, PERIOD valid_at) REFERENCES (id, valid_at)
+		REFERENCES temporal_rng (id, valid_at)
+-- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at)
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at)
+-- (parent_id, PERIOD valid_at) REFERENCES (id)
+		REFERENCES temporal_rng (id)
+-- (parent_id) REFERENCES (id, PERIOD valid_at)
+	CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id)

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

 doc/src/sgml/ref/delete.sgml                  |  45 ++
 doc/src/sgml/ref/update.sgml                  |  46 ++
 src/backend/executor/nodeModifyTable.c        | 226 ++++++++-
 src/backend/parser/analyze.c                  | 175 ++++++-
 src/test/regress/expected/for_portion_of.out  | 459 ++++++++++++++++++
 .../regress/expected/without_overlaps.out     | 144 +++++-
 src/test/regress/sql/for_portion_of.sql       | 359 ++++++++++++++
 47 files changed, 2198 insertions(+), 51 deletions(-)

index 1b81b4e7d7..eb57a9b1d5 100644
 
+  <para>
@@ -116,6 +126,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 2ab24b0523..e5764a0690 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> FROM <replaceable class="parameter">start_time</replaceable> TO <replaceable class="parameter">end_time</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,16 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
@@ -114,6 +125,41 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+      The range column or period to use when performing a temporal update.
+      <replaceable class="parameter">range_or_period_name</replaceable>. A
+      <literal>NULL</literal> here indicates an update whose beginning is
+      unbounded (as with range types).
+      <replaceable class="parameter">range_or_period_name</replaceable>. A
+      <literal>NULL</literal> here indicates an update whose end is unbounded
+      (as with range types).

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4cd4ab9dd0..480368ad9e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12285,6 +12285,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

index a10a42b97f..069c22b38e 100644

 

 

+/* ----------
+ * ForPortionOfState()
+ *
 
index b16fbe9e22..88eda01280 100644

 
 
@@ -3626,6 +3780,7 @@ ExecModifyTable(PlanState *pstate)
@@ -4299,6 +4454,75 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*

 

index 7a1dfb6364..db3b1d457f 100644
 
@@ -60,10 +62,16 @@
 
@@ -479,6 +487,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -512,6 +534,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -550,7 +573,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -1173,7 +1200,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1230,123 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+			list_make2(forPortionOf->target_start, forPortionOf->target_end),
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2553,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2426,6 +2571,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2442,7 +2591,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2451,7 +2601,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2470,7 +2620,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2520,6 +2670,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 9a6f3f647a..98546b21e7 100644
@@ -550,6 +551,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -746,7 +748,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 	QUOTE
@@ -827,6 +829,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %nonassoc	KEYS OBJECT_P SCALAR VALUE_P
 %nonassoc	WITH WITHOUT
 
+/*
@@ -12202,14 +12214,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;
 				}
@@ -12272,6 +12286,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
@@ -12280,10 +12295,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;
 				}
@@ -13719,6 +13735,19 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+for_portion_of_clause:
+			FOR PORTION OF ColId FROM a_expr TO a_expr
+				{
+					ForPortionOfClause *n = makeNode(ForPortionOfClause);
+					n->range_name = $4;
+					n->range_name_location = @4;
+					n->target_start = $6;
+					n->target_end = $8;
+					$$ = n;
+				}
+			| /*EMPTY*/					{ $$ = NULL; }
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -17281,6 +17310,7 @@ unreserved_keyword:
 			| PASSWORD
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -17885,6 +17915,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 9bbad33fbd..bff10c9c78 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;
 
 			/*
@@ -964,6 +971,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 					/*
index 199eae525d..d04c75b398 100644
 
+/*
 
 Datum
index d00c6e5334..55b3248b78 100644

 /*

 

 

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

+/*
+ * ForPortionOfState
+ *

diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index adfc5a223c..d462cccf56 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);
 
@@ -1530,6 +1533,19 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -1893,12 +1909,13 @@ typedef struct InsertStmt
 
@@ -1907,13 +1924,14 @@ typedef struct DeleteStmt
 
index d40af8e59f..ed8ff7318e 100644
index bb930afb52..c083852b03 100644
@@ -2068,4 +2068,29 @@ typedef struct OnConflictExpr
 
+/*----------

index 0000000000..32f00cdbf5
@@ -0,0 +1,459 @@
+FOR PORTION OF valid_at FROM '2019-01-15' TO NULL;
+FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL
+FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL
+FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL;
+FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL;
+FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
index fbb0489a4f..3395d2150f 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 12d2ebd6a5..80a4e60695 100644
@@ -292,6 +292,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
@@ -344,6 +374,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -379,6 +439,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -669,13 +759,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -706,13 +808,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -734,9 +848,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
@@ -758,9 +885,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
index b08aaae44b..cc2f6b46e0 100644
 
 # ----------
index 0000000000..e65968fbf4
+FOR PORTION OF valid_at FROM '2019-01-15' TO NULL;
+FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL
+FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL
+FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL;
+FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL;
+FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+FOR PORTION OF valid_at FROM lower(valid_at) TO NULL
+FOR PORTION OF valid_at FROM '2018-06-01' TO NULL
+FOR PORTION OF valid_at FROM NULL TO '2018-03-01'
+FOR PORTION OF valid_at FROM NULL TO '2018-02-01'
+FOR PORTION OF valid_at FROM '2017-01-01' TO NULL
index 3f68cafcd1..e10added9d 100644

index 21241ef9c2..250ee71b1c 100644

 -- temporal UNIQUE:
 
 --
@@ -539,13 +585,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 
@@ -574,13 +630,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -600,14 +666,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 
@@ -624,9 +700,20 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-

From 1e93e268d327f5110859c5b70444eabeb3b328d6 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v18 4/5] 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 480368ad9e..030ff4adab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -505,7 +505,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 *fkperiodattnums,
 										 int numfksetcols, const int16 *fksetcolsattnums,
 										 List *fksetcols);
 static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -9653,7 +9653,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, fkperiodattnums,
 								 numfkdelsetcols, fkdelsetcols,
 								 fkconstraint->fk_del_set_cols);
 
@@ -9825,12 +9825,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
+							 const int16 *fkperiodattnums,
 							 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 = fkperiodattnums[0];
 		bool		seen = false;
 
 		for (int j = 0; j < numfks; j++)
@@ -9840,6 +9843,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)
@@ -12420,11 +12430,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12511,11 +12529,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 55b3248b78..70ace70e7a 100644

+/*
 
+/*
 
 	/*
index e70b391e04..545a6b1ca0 100644
index 80a4e60695..a011b7dce5 100644
@@ -904,30 +904,184 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
@@ -935,7 +1089,461 @@ ALTER TABLE temporal_fk_rng2rng
+-- fails because you can't set the PERIOD column:
+		ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
+-- fails because you can't set the PERIOD column:
+		ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+ERROR:  column "valid_at" referenced in ON DELETE SET action cannot be PERIOD
+-- ok:
@@ -943,8 +1551,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -957,8 +1565,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -993,7 +1601,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1005,7 +1613,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
@@ -1027,7 +1635,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1039,35 +1647,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
index 250ee71b1c..bdb6a6177c 100644
@@ -718,31 +718,114 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');

 
@@ -750,6 +833,271 @@ ALTER TABLE temporal_fk_rng2rng
+-- fails because you can't set the PERIOD column:
+		ON DELETE SET NULL (valid_at) ON UPDATE SET NULL;
+-- ok:
+-- fails because you can't set the PERIOD column:
+		ON DELETE SET DEFAULT (valid_at) ON UPDATE SET DEFAULT;
+-- ok:

@@ -759,8 +1107,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -774,8 +1122,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -859,37 +1207,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 

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

index 618a05ac89..450f35f9d5 100644
 
+     <row>
@@ -5723,6 +5728,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
index 4490e82aa5..cb6a4983be 100644

 

index b5f48525de..b00bd80cf5 100644

 [ 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>
@@ -800,6 +816,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index ec7b6f5362..466f57095c 100644
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index 6b6609e23a..dd129ab4b3 100644
 
+/*

 /*
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case OCLASS_PERIOD:
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 80c40eaf57..2c1d1a10a7 100644
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
index 030ff4adab..366dcd6802 100644

 
+	/*
 
+	/*
 
 	/*
 
+/*

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

 
 /*

 
+/*
@@ -8095,6 +8676,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -13630,6 +14360,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15693,7 +16432,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index 98546b21e7..02218c5449 100644
@@ -594,7 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
@@ -2600,6 +2600,24 @@ alter_table_cmd:
@@ -3719,8 +3737,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4072,6 +4092,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7130,6 +7163,14 @@ CommentStmt:

index 6030ba90cd..a9531bc8a7 100644
 
 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:
@@ -347,6 +356,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
@@ -913,6 +923,113 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
 	}
 }
 
+void
@@ -1003,6 +1120,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1055,6 +1173,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1064,10 +1183,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
@@ -3047,6 +3174,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.)
@@ -3504,6 +3635,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;
@@ -3566,6 +3698,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*
index 1709007dc6..168264c6ed 100644
@@ -6352,6 +6352,7 @@ getTables(Archive *fout, int *numTables)
@@ -6429,6 +6430,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6566,6 +6575,7 @@ getTables(Archive *fout, int *numTables)
@@ -6649,6 +6659,7 @@ getTables(Archive *fout, int *numTables)
@@ -8383,7 +8394,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8436,6 +8447,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8450,7 +8463,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8986,15 +9000,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9016,6 +9051,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9035,12 +9071,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9099,6 +9136,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10369,6 +10479,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15904,6 +16016,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -15912,7 +16050,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16130,7 +16268,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16432,7 +16570,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18517,6 +18655,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 cbf4b5e179..b137102c7e 100644

index 61d6452b53..3ba16d67fd 100644
@@ -1943,6 +1943,8 @@ describeOneTableDetails(const char *schemaname,

 	res = PSQLexec(buf.data);
@@ -2365,6 +2367,40 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/* print periods */

 
 /*

 

 #endif							/* TABLECMDS_H */
index d462cccf56..78b3b86efe 100644
@@ -2139,6 +2139,7 @@ typedef enum ObjectType
@@ -2226,6 +2227,8 @@ typedef enum AlterTableType
@@ -2492,11 +2495,11 @@ typedef struct VariableShowStmt
 
@@ -2505,6 +2508,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 */
@@ -2519,6 +2523,30 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3226,6 +3254,7 @@ typedef struct IndexStmt
 
index cc2f6b46e0..d6b8b96d88 100644
 
 # ----------

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 18 Nov 2023 21:24:09 -0800

Thank you for continuing to review this submission! My changes are in
the v18 patch I sent a few days ago. Details below.

> foreign key columns is "generated columns".

I don't see anything like that included in your attachment. I do see
the restriction on `ON DELETE SET NULL/DEFAULT (columnlist)`, which I
included. But you are referring to something else I take it? Why do
you think FKs should fail if the referred column is GENERATED? Is that
a restriction you think should apply to all FKs or only temporal ones?

> best way to fix it.
> . . .

Fixed, with additional tests re PERIOD on one side but not the other.

> make pg_constraint record correct {conpfeqop,conppeqop,conffeqop}.

This change is incorrect because it causes foreign keys to fail when
created with btree_gist. See my reply to Peter for more about that. My
v18 patch also includes some new (very simple) tests in the btree_gist
extension so it's easier to see whether temporal PKs & FKs work there.

> --ON DELETE SET NULL(valid_at);

Okay, thanks!

> * refactor restrict_cascading_range function.

It looks like your attachment only renames the column, but I think
"restrict" is more expressive and accurate than "get", so I'd like to
keep the original name here.

> 1;}, So I changed the code order to make the error report more

Since we do numfks +=1 and numpks +=1, I don't see any inconsistency
here. Also you are making things now happen before a permissions
check, which may be important (I'm not sure). Can you explain what
improvement is intended here? Your changes don't seem to cause any
changes in the tests, so what is the goal? Perhaps I'm
misunderstanding what you mean by "more consistent."

Thanks! I'll reply to your Nov 6 email separately.

Yours,

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 18 Nov 2023 21:32:52 -0800

> The first "para" should be <para> ?

Thanks, fixed (in v18)!

> Does that mean oue new change in gram.y is somehow wrong?

Fixed (in read+out node funcs).

> Is the second sentence unnecessary? since no primary can still do "for

You're right, this dates back to an older version of the patch. Removed.

> sql-delete.html reference also needs to change.

See below re UNBOUNDED....

> "if (nodeTag(n) == T_ColumnRef)" part.

You're right, using a ColumnDef was probably not good here, and
treating `"UNBOUNDED"` (with quotes from the user) as a keyword is no
good. I couldn't find a way to make this work without reduce/reduce
conflicts, so I just took it out. It was syntactic sugar for `FROM/TO
NULL` and not part of the standard, so it's not too important. Also I
see that UNBOUNDED causes difficult problems already with window
functions (comments in gram.y). I hope I can find a way to make this
work eventually, but it can go for now.

> info into <varlistentry id="sql-createtable-parms-unique">

You're right, fixed (though Peter's patch then changed this same spot).

Thanks,

---

From: jian he <[email-masked]>
Date: Mon, 20 Nov 2023 14:57:37 +0800

> a restriction you think should apply to all FKs or only temporal ones?

I believe the following part should fail. Similar tests on
src/test/regress/sql/generated.sql. line begin 347.

drop table if exists gtest23a,gtest23x cascade;
CREATE TABLE gtest23a (x int4range, y int4range,
CONSTRAINT gtest23a_pk PRIMARY KEY (x, y WITHOUT OVERLAPS));
CREATE TABLE gtest23x (a int4range, b int4range GENERATED ALWAYS AS
('empty') STORED,
FOREIGN KEY (a, PERIOD b ) REFERENCES gtest23a(x, PERIOD y) ON UPDATE
CASCADE);  -- should be error?
-------

> misunderstanding what you mean by "more consistent."

begin;
drop table if exists fk, pk cascade;
CREATE TABLE pk (id int4range, valid_at int4range,
CONSTRAINT pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
CREATE TABLE fk (
id int4range,valid_at tsrange, parent_id int4range,
CONSTRAINT fk FOREIGN KEY (parent_id, valid_at)
    REFERENCES pk
);
rollback;
--
the above query will return an error: number of referencing and
referenced columns for foreign key disagree.
but if you look at it closely, primary key and foreign key columns both are two!
The error should be saying valid_at should be specified with "PERIOD".

begin;
drop table if exists fk, pk cascade;
CREATE TABLE pk (id int4range, valid_at int4range,
CONSTRAINT pk_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
CREATE TABLE fk (
id int4range,valid_at int4range, parent_id int4range,
CONSTRAINT fk FOREIGN KEY (parent_id, period valid_at)
REFERENCES pk
);
select conname,array_length(conkey,1),array_length(confkey,1)
from pg_constraint where conname = 'fk';
rollback;
------------
I found out other issues in v18.
I first do `git apply` then  `git diff --check`, there is a white
space error in v18-0005.

You also need to change update.sgml and delete.sgml <title>Outputs</title> part.
Since at most, it can return 'UPDATE 3' or 'DELETE 3'.

--the following query should work?
drop table pk;
CREATE table pk(a numrange PRIMARY key,b text);
insert into pk values('[1,10]');
create or replace function demo1() returns void as $$
declare lb numeric default 1; up numeric default 3;
begin
    update pk for portion of a from lb to up set b = 'lb_to_up';
    return;
end
$$ language plpgsql;
select * from demo1();

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 20 Nov 2023 08:58:55 +0100

On 17.11.23 19:39, Paul Jungwirth wrote:
> But I feel the overall approach is wrong: originally I used hardcoded 
> "=" and "&&" operators, and you asked me to look them up by strategy 
> number instead. But that leads to trouble with core gist types vs 
> btree_gist types. The core gist opclasses use RT*StrategyNumbers, but 
> btree_gist creates opclasses with BT*StrategyNumbers.

Ouch.

That also provides the answer to my question #2 here: 
https://www.postgresql.org/message-id/6f010a6e-8e20-658b-dc05-dc9033a694da%40eisentraut.org

I don't have a good idea about this right now.  Could we just change 
btree_gist perhaps?  Do we need a new API for this somewhere?

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 23 Nov 2023 10:08:54 +0100

> btree_gist perhaps?  Do we need a new API for this somewhere?

After further thought, I think the right solution is to change 
btree_gist (and probably also btree_gin) to use the common RT* strategy 
numbers.  The strategy numbers are the right interface to determine the 
semantics of index AM operators.  It's just that until now, nothing 
external has needed this information from gist indexes (unlike btree, 
hash), so it has been a free-for-all.

I don't see an ALTER OPERATOR CLASS command that could be used to 
implement this.  Maybe we could get away with a direct catalog UPDATE. 
Or we need to make some DDL for this.

Alternatively, this could be the time to reconsider moving this into core.

---

From: Paul Jungwirth <[email-masked]>
Date: Sat, 2 Dec 2023 10:11:52 -0800

Thank you again for such thorough reviews!

 > Similarly, we also need to apply the above logic to DELETE FOR PORTION OF.

I don't think UPDATE/DELETE FOR PORTION OF is supposed to require INSERT permission.

Notionally the INSERTs are just to preserve what was there already, not to add new data.
The idea is that a temporal table is equivalent to a table with one row for every "instant",
i.e. one row per microsecond/second/day/whatever-time-resolution. Of course that would be too slow,
so we use PERIODs/ranges instead, but the behavior should be the same. Date's book has a good 
discussion of this idea.

I also checked the SQL:2011 draft standard, and there is a section called Access Rules in Part 2: 
SQL/Foundation for UPDATE and DELETE statements. Those sections say you need UPDATE/DELETE 
privileges, but say nothing about needing INSERT privileges. That is on page 949 and 972 of the PDFs 
from the "SQL:20nn Working Draft Documents" link at [1]. If someone has a copy of SQL:2016 maybe 
something was changed, but I would be surprised.

I also checked MariaDB and IBM DB2, the only two RDBMSes that implement FOR PORTION OF to my 
knowledge. (It is not in Oracle or MSSQL.) I created a table with one row, then gave another user 
privileges to SELECT & UPDATE, but not INSERT. In both cases, that user could execute an UPDATE FOR 
PORTION OF that resulted in new rows, but could not INSERT genuinely new rows. [2,3]

So instead of changing this I've updated the documentation to make it explicit that you do not need 
INSERT privilege to use FOR PORTION OF. I also documented which triggers will fire and in which order.

 > similarly the doc/src/sgml/ref/delete.sgml the link reference also broken.

Okay, changed.

 > before row insert, after row insert. after row update.

Okay, reworked the docs for this.

Changed.

v19 patch series attached, rebased to a11c9c42ea.

[1] https://web.archive.org/web/20230923221106/https://www.wiscorp.com/SQLStandards.html

[2] MariaDB test:

First create a table as the root user:

```
create table t (id int, ds date, de date, name text, period for valid_at (ds, de));
insert into t values (1, '2000-01-01', '2001-01-01', 'foo');
```

and give another user select & update privlege (but not insert):

```
create database paul;
use paul;
create user 'update_only'@'localhost' identified by 'test';
grant select, update on paul.t to 'update_only'@'localhost';
flush privileges;
```

Now as that user:

```
mysql -uupdate_only -p
use paul;
-- We can update the whole record:
update t for portion of valid_at from '2000-01-01' to '2001-01-01' set name = 'bar';
-- We can update a part of the record:
update t for portion of valid_at from '2000-01-01' to '2000-07-01' set name = 'baz';
select * from t;
+------+------------+------------+------+
| id   | ds         | de         | name |
+------+------------+------------+------+
|    1 | 2000-01-01 | 2000-07-01 | baz  |
|    1 | 2000-07-01 | 2001-01-01 | bar  |
+------+------------+------------+------+
-- We cannot insert:
insert into t values (2, '2000-01-01', '2001-01-01' 'another');
ERROR 1142 (42000): INSERT command denied to user 'update_only'@'localhost' for table `paul`.`t`
```

[3] IBM DB2 test:

```
mkdir ~/local/db2
cd ~/local/db2
tar xzvf ~/Downloads/v11.5.9_linuxx64_server_dec.tar.gz
cd server_dev
./db2_install # should put something at ~/sqllib
source ~/sqllib/db2profile
db2start  # but I got "The database manager is already active."
db2
create database paul -- first time only, note no semicolon
connect to paul
create table t (id integer, ds date not null, de date not null, name varchar(4000), period 
business_time (ds, de));
insert into t values (1, '2000-01-01', '2001-01-01', 'foo');
grant connect on database to user james;
grant select, update on t to user james;
```

Now as james:

```
source ~paul/sqllib/db2profile
db2
connect to paul
select * from paul.t;
update paul.t for portion of business_time from '2000-01-01' to '2000-06-01' set name = 'bar';
DB20000I  The SQL command completed successfully.
select * from paul.t;
insert into paul.t values (2, '2000-01-01', '2001-01-01', 'bar');
DB21034E  The command was processed as an SQL statement because it was not a
valid Command Line Processor command.  During SQL processing it returned:
SQL0551N  The statement failed because the authorization ID does not have the
required authorization or privilege to perform the operation.  Authorization
ID: "JAMES".  Operation: "INSERT". Object: "PAUL.T".  SQLSTATE=42501
```

Yours,

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

 

 
+     <row>

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

      <para>
 
      <para>
 
 	pfree(ccbin);

 
 	/*
 
 	if (conkeyArray)
index 795f29d02d..d044114ad7 100644

 
+	/*

 	/*
@@ -1027,12 +1043,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -1041,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
@@ -1185,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,
@@ -1826,6 +1844,97 @@ CheckPredicate(Expr *predicate)
 				 errmsg("functions in index predicate must be marked IMMUTABLE")));
 }
 
+/*
@@ -1849,6 +1958,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
@@ -1872,6 +1982,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 	else
 		nextExclOp = NULL;

@@ -2148,6 +2266,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 
 		/*
index 7206da7c53..ec6cd00138 100644
@@ -10090,6 +10090,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10388,6 +10389,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10893,6 +10895,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conWithoutOverlaps */
 								  true);
 
@@ -14281,7 +14284,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)

 
 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
index d631ac89a9..abca89a7c9 100644
@@ -4117,7 +4117,7 @@ ConstraintElem:
@@ -4126,11 +4126,12 @@ ConstraintElem:
@@ -4151,7 +4152,7 @@ ConstraintElem:
@@ -4159,11 +4160,12 @@ ConstraintElem:
@@ -4231,6 +4233,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 
+opt_without_overlaps:

 
+		/*

 	/*

 

 

 
 
 	return nKeys;
 }

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..524df52a16 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6994,6 +6994,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conwithoutoverlaps,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7075,10 +7076,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7146,6 +7154,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7253,6 +7262,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -16954,6 +16964,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conwithoutoverlaps)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");

 

+	/*
 
 	/*
 
 	/*

 

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

 
+-- Foreign key

 
index ec6cd00138..eaa9a2b128 100644
@@ -5895,7 +5908,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
@@ -9524,6 +9538,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9618,6 +9637,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9636,7 +9664,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9644,8 +9674,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9704,187 +9748,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
 	/*
@@ -9903,7 +9787,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
@@ -9919,7 +9804,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10004,7 +9890,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10090,7 +9977,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10166,7 +10053,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10224,7 +10112,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10272,6 +10161,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10389,7 +10279,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10420,7 +10310,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10656,7 +10547,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
@@ -10895,7 +10787,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
@@ -10929,13 +10821,245 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)

+static void
@@ -11433,7 +11557,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11680,7 +11808,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11746,7 +11873,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;
@@ -11811,36 +11940,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11867,6 +12015,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11888,12 +12040,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11931,6 +12087,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12040,7 +12209,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12069,8 +12239,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;
 
 	/*
@@ -12139,6 +12311,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12158,12 +12331,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12221,37 +12400,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12281,37 +12491,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
index abca89a7c9..5b110ca7fe 100644

 
@@ -4206,21 +4207,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
@@ -4248,6 +4251,16 @@ columnList:
 
+optionalPeriodName:
@@ -17585,6 +17598,7 @@ reserved_keyword:
@@ -17894,6 +17908,7 @@ bare_label_keyword:

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

 

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

 

 

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

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

 

 

 

 
 	/*
 
 	/*
 
 	/*

 

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

 doc/src/sgml/ref/delete.sgml                  |  70 +++
 doc/src/sgml/ref/update.sgml                  |  72 +++
 src/backend/parser/gram.y                     |  48 +-
 src/backend/utils/cache/lsyscache.c           |  53 +++
 src/test/regress/expected/for_portion_of.out  | 443 ++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql       | 343 ++++++++++++++
 47 files changed, 2218 insertions(+), 51 deletions(-)

index 1b81b4e7d7..389de5bc06 100644
@@ -54,6 +55,40 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
    circumstances.
   </para>
 
+  <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>
+
   <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 +151,41 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
index 2ab24b0523..7ca99ba3d5 100644
@@ -51,6 +52,42 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <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 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 +151,41 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eaa9a2b128..4b6ecb51cb 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12282,6 +12282,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

 

 

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

 

 
+	/*

 

 
@@ -60,10 +62,16 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*
index 5b110ca7fe..d54d84adf6 100644

 
 	QUOTE
@@ -864,6 +866,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
@@ -12215,14 +12228,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;
 				}
@@ -12285,6 +12300,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
@@ -12293,10 +12309,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;
 				}
@@ -13732,6 +13749,19 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+for_portion_of_clause:
@@ -17308,6 +17338,7 @@ unreserved_keyword:
@@ -17912,6 +17943,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*

 					/*
 
+/*
 
 Datum

 /*

 

 

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

+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

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

index 0000000000..64f7bc7e02
@@ -0,0 +1,443 @@
+CREATE TRIGGER trg_for_portion_of_before
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+CREATE TRIGGER trg_for_portion_of_after
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 # ----------
index 0000000000..677b8e38dd
@@ -0,0 +1,343 @@
+CREATE TRIGGER trg_for_portion_of_before
+  BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test
+CREATE TRIGGER trg_for_portion_of_after
+  AFTER INSERT OR UPDATE OR DELETE ON for_portion_of_test

 
 
 -- temporal UNIQUE:

 

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

---

index 4b6ecb51cb..46c148ce41 100644
@@ -9650,7 +9650,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9822,12 +9822,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 
@@ -9837,6 +9840,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12417,11 +12427,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12508,11 +12526,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*

 

 

 

 

 

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

 
+     <row>
index d2951cd754..1e091fbf7d 100644

index 0ca7d5a9e0..56a4464ab7 100644

index e1d207bc60..3dc01019a9 100644

 
 
+  <para>

+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+

+/*

 /*
 
+		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 46c148ce41..eed214ab3b 100644

 
+	/*
 
+	/*
 
 	/*
 
+/*
@@ -3344,6 +3656,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4400,12 +4874,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4414,7 +4888,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4507,6 +4981,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4822,6 +5298,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5219,6 +5703,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6363,6 +6855,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6386,6 +6880,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7391,14 +7887,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7442,6 +7953,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -8092,6 +8673,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -13627,6 +14357,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15690,7 +16429,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index d54d84adf6..ffed5101e2 100644
 
@@ -2614,6 +2614,24 @@ alter_table_cmd:
@@ -3733,8 +3751,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4086,6 +4106,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7144,6 +7177,14 @@ CommentStmt:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*
index 524df52a16..b8952e0155 100644
@@ -6361,6 +6361,7 @@ getTables(Archive *fout, int *numTables)
@@ -6438,6 +6439,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6575,6 +6584,7 @@ getTables(Archive *fout, int *numTables)
@@ -6658,6 +6668,7 @@ getTables(Archive *fout, int *numTables)
@@ -8392,7 +8403,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8445,6 +8456,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8459,7 +8472,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8995,15 +9009,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9025,6 +9060,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9044,12 +9080,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9108,6 +9145,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10378,6 +10488,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15913,6 +16025,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -15921,7 +16059,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16139,7 +16277,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16441,7 +16579,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

 

 
 
+		/* print periods */

 
 /*

 

 #endif							/* TABLECMDS_H */

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

 # ----------

---

From: Paul Jungwirth <[email-masked]>
Date: Sat, 2 Dec 2023 10:41:01 -0800

On Thu, Nov 23, 2023 at 1:08 AM Peter Eisentraut <[email-masked]> wrote:
 > numbers.

Okay. That will mean bumping the version of btree_gist, and you must be running that version to use 
the new temporal features, or you will get silly results. Right? Is there a way to protect users 
against that and communicate they need to upgrade the extension?

This also means temporal features may not work in custom GIST opclasses. What we're saying is they 
must have an appropriate operator for RTEqualStrategyNumber (18) and RTOverlapStrategyNumber (3). 
Equal matters for the scalar key part(s), overlap for the range part. So equal is more likely to be 
an issue, but overlap matters if we want to support non-ranges (which I'd say is worth doing).

Also if they get it wrong, we won't really have any way to report an error.

I did some research on other extensions in contrib, as well as PostGIS. Here is what I found:

## btree_gin:

3 is =
18 is undefined

same for all types: macaddr8, int2, int4, int8, float4, float8, oid, timestamp, timestamptz, time, 
timetz, date, interval, inet, cidr, text, varchar, char, bytea, bit, varbit, numeric, anyenum, uuid, 
name, bool, bpchar

## cube

3 is &&
18 is <=>

## intarray

3 is &&
18 is undefined

## ltree

3 is =
18 is undefined

## hstore

3 and 18 are undefined

## seg

3 is &&
18 is undefined

## postgis: geometry

3 is &&
18 is undefined

## postgis: geometry_nd

3 is &&&
18 is undefined

I thought about looking through pgxn for more, but I haven't yet. I may still do that.
But already it seems like there is not much consistency.

So what do you think of this idea instead?:

We could add a new (optional) support function to GiST that translates "well-known" strategy numbers 
into the opclass's own strategy numbers. This would be support function 12. Then we can say 
translateStrategyNumber(RTEqualStrategyNumber) and look up the operator with the result.

There is not a performance hit, because we do this for the DDL command (create pk/uq/fk), then store 
the operator in the index/constraint.

If you don't provide this new support function, then creating the pk/uq/fk fails with a hint about 
what you can do to make it work.

This approach means we don't change the rules about GiST opclasses: you can still use the stranums 
how you like.

This function would also let me support non-range "temporal" foreign keys, where I'll need to build 
queries with && and maybe other operators.

What do you think?

---

From: Vik Fearing <[email-masked]>
Date: Sun, 3 Dec 2023 11:13:57 +0100

On 12/2/23 19:11, Paul Jungwirth wrote:
> so we use PERIODs/ranges instead, but the behavior should be the same. 
> Date's book has a good discussion of this idea.
> 
> I also checked the SQL:2011 draft standard, and there is a section 
> called Access Rules in Part 2: SQL/Foundation for UPDATE and DELETE 
> statements. Those sections say you need UPDATE/DELETE privileges, but 
> say nothing about needing INSERT privileges. That is on page 949 and 972 
> of the PDFs from the "SQL:20nn Working Draft Documents" link at [1]. If 
> someone has a copy of SQL:2016 maybe something was changed, but I would 
> be surprised

Nothing has changed here in SQL:2023 (or since).
-- 
Vik Fearing

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 6 Dec 2023 09:59:33 +0100

On 02.12.23 19:41, Paul Jungwirth wrote:
> We could add a new (optional) support function to GiST that translates 
> "well-known" strategy numbers into the opclass's own strategy numbers. 
> This would be support function 12. Then we can say 
> There is not a performance hit, because we do this for the DDL command 
> (create pk/uq/fk), then store the operator in the index/constraint.
> 
> If you don't provide this new support function, then creating the 
> pk/uq/fk fails with a hint about what you can do to make it work.
> 
> This approach means we don't change the rules about GiST opclasses: you 
> can still use the stranums how you like.
> 
> This function would also let me support non-range "temporal" foreign 
> keys, where I'll need to build queries with && and maybe other operators.

I had some conversations about this behind the scenes.  I think this 
idea makes sense.

The other idea was that we create new strategy numbers, like 
TemporalEqualsStrategy / TemporalOverlapsStrategy.  But then you'd have 
the situation where some strategy numbers are reserved and others are 
not, so perhaps that is not so clean.  I think your idea is good.

---

From: jian he <[email-masked]>
Date: Wed, 6 Dec 2023 21:22:07 +0800

> v19 patch series attached, rebased to a11c9c42ea.

this TODO:
 * TODO: It sounds like FOR PORTION OF might need to do something here too?
based on comments on ExprContext. I refactor a bit, and solved this TODO.

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.
From 8c97db9391900d766db99834c23c5e4b60cadf01 Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Tue, 5 Dec 2023 17:59:43 +0800
Subject: [PATCH v1 1/1] set eval targetrange in per-output-tuple exprcontext

---
 src/backend/executor/nodeModifyTable.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 88eda012..fe57e592 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3780,7 +3780,6 @@ ExecModifyTable(PlanState *pstate)
 		 * Reset per-tuple memory context used for processing on conflict and
 		 * returning clauses, to free any expression evaluation storage
 		 * allocated in the previous cycle.
-		 * TODO: It sounds like FOR PORTION OF might need to do something here too?
 		 */
 		if (pstate->ps_ExprContext)
 			ResetExprContext(pstate->ps_ExprContext);
@@ -4472,9 +4471,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		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;
+		econtext = GetPerTupleExprContext(estate);
 
 		exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
 		targetRange = ExecEvalExpr(exprState, econtext, &isNull);
-- 
2.34.1

From cda2f8324ef920c807008e2763f97c6503c17a94 Mon Sep 17 00:00:00 2001
From: pgaddict <[email-masked]>
Date: Wed, 6 Dec 2023 20:58:08 +0800
Subject: [PATCH v1 1/1] trying to save mt_transition_capture while ExecInsert

---
 src/backend/executor/nodeModifyTable.c       | 21 ++++-
 src/test/regress/expected/for_portion_of.out | 80 ++++++++++++++++++++
 src/test/regress/sql/for_portion_of.sql      | 72 ++++++++++++++++++
 3 files changed, 171 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index fe57e592..c9d14dab 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -167,7 +167,8 @@ static bool ExecMergeMatched(ModifyTableContext *context,
 static void ExecMergeNotMatched(ModifyTableContext *context,
 								ResultRelInfo *resultRelInfo,
 								bool canSetTag);
-
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate);

 /*
  * Verify that the tuples to be produced by INSERT match the
@@ -1259,7 +1260,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	TupleTableSlot *leftoverTuple2 = fpoState->fp_Leftover2;
 	HeapTuple oldtuple = NULL;
 	bool shouldFree = false;
-
+	int		i;	/* original cmd type */
 	/*
 	 * Get the range of the old pre-UPDATE/DELETE tuple,
 	 * so we can intersect it with the FOR PORTION OF target
@@ -1312,6 +1313,7 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 	 * TODO: Really? What if you update the partition key?
 	 */

+	i	= context->mtstate->operation;
 	if (!RangeIsEmpty(leftoverRangeType1))
 	{
 		oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, &shouldFree);
@@ -1320,6 +1322,10 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 		set_leftover_tuple_bounds(leftoverTuple1, forPortionOf, typcache, leftoverRangeType1);
 		ExecMaterializeSlot(leftoverTuple1);

+		context->mtstate->operation = CMD_INSERT;
+		context->mtstate->mt_transition_capture = NULL;
+		ExecSetupTransitionCaptureState(context->mtstate, estate);
+
 		// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
 		ExecInsert(context, resultRelInfo, leftoverTuple1, node->canSetTag, NULL, NULL);
 	}
@@ -1340,10 +1346,21 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
 		set_leftover_tuple_bounds(leftoverTuple2, forPortionOf, typcache, leftoverRangeType2);
 		ExecMaterializeSlot(leftoverTuple2);

+		context->mtstate->operation = CMD_INSERT;
+		context->mtstate->mt_transition_capture = NULL;
+		ExecSetupTransitionCaptureState(context->mtstate, estate);
+
 		// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
 		ExecInsert(context, resultRelInfo, leftoverTuple2, node->canSetTag, NULL, NULL);
 	}

+	if (!RangeIsEmpty(leftoverRangeType2) || !RangeIsEmpty(leftoverRangeType1))
+	{
+		/* if any of the above branch executed then */
+		context->mtstate->operation = i;
+		context->mtstate->mt_transition_capture = NULL;
+		ExecSetupTransitionCaptureState(context->mtstate, estate);
+	}
 	if (shouldFree)
 		heap_freetuple(oldtuple);
 }
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 64f7bc7e..04202aa0 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -441,3 +441,83 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;

 DROP TABLE for_portion_of_test2;
 DROP TYPE mydaterange;
+DROP TABLE IF EXISTS 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,1]', '[2018-01-01,2020-01-01)', 'one');
+create or replace function dump_trigger() returns trigger language plpgsql as
+$$
+	BEGIN
+    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);
+	END IF;
+    RETURN NULL;
+	END;
+$$;
+create or replace 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 or replace 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 or replace 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 or replace 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 or replace trigger for_portion_of_test_del_trig
+after delete on for_portion_of_test
+referencing old table as old_table
+for each row execute procedure dump_trigger();
+create or replace trigger for_portion_of_test_del_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:  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;
+begin;
+delete from for_portion_of_test 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_del_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_del_trig_stmt,TG_LEVEL STATEMENT, old table = ("[1,2)","[01-01-2018,01-01-2020)",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:  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 TABLE IF EXISTS for_portion_of_test;
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index 677b8e38..e86819c4 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -341,3 +341,75 @@ SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at;

 DROP TABLE for_portion_of_test2;
 DROP TYPE mydaterange;
+
+DROP TABLE IF EXISTS 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,1]', '[2018-01-01,2020-01-01)', 'one');
+
+create or replace function dump_trigger() returns trigger language plpgsql as
+$$
+	BEGIN
+    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);
+	END IF;
+    RETURN NULL;
+	END;
+$$;
+
+create or replace 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 or replace 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 or replace 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 or replace 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 or replace trigger for_portion_of_test_del_trig
+after delete on for_portion_of_test
+referencing old table as old_table
+for each row execute procedure dump_trigger();
+
+create or replace trigger for_portion_of_test_del_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';
+rollback;
+
+begin;
+delete from for_portion_of_test FOR PORTION OF valid_at FROM NULL TO '2018-01-21';
+rollback;
+
+begin;
+UPDATE for_portion_of_test FOR PORTION OF valid_at
+	FROM NULL TO '2018-01-02' SET name = 'NULL_to_2018-01-01';
+rollback;
+
+DROP TABLE IF EXISTS for_portion_of_test;
\ No newline at end of file
--
2.34.1

---

From: jian he <[email-masked]>
Date: Mon, 11 Dec 2023 16:31:24 +0800

hi. some small issues....

Is the above part unnecessary?

+Datum period_to_range(TupleTableSlot *slot, int startattno, int
endattno, Oid rangetype)

I am confused. so now I only apply v19, 0001 to 0003.
period_to_range function never used. maybe we can move this part to
0005-Add PERIODs.patch?
Also you add change in Makefile in 0003, meson.build change in 0005,
better put it on in 0005?

+/*

this part will never happen?
since "FROM INTERVAL YEAR TO MONTH TO"
means "valid_at" will be interval range data type, which does not exist now.

@@ -1258,6 +1277,7 @@ ri_set(TriggerData *trigdata, bool is_set_null,
int tgkind)
  ri_PerformCheck(riinfo, &qkey, qplan,
  fk_rel, pk_rel,
  oldslot, NULL,
+ -1, 0,
  true, /* must detect new rows */
  SPI_OK_UPDATE);

for all the refactor related to ri_PerformCheck, do you need (Datum) 0
instead of plain 0?

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

https://influentialpoints.com/Training/basic_statistics_ranges.htm#:~:text=A%20range%20is%20two%20numbers,or%20the%20difference%20between%20them
So "range" is more accurate than "interval"?

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

should it be "Copy a ForPortionOfState into the TopTransactionContext"?

---

From: Paul Jungwirth <[email-masked]>
Date: Sun, 31 Dec 2023 00:51:49 -0800

> I had some conversations about this behind the scenes.  I think this

Here is a patch series with the GiST stratnum support function added. I put this into a separate 
patch (before all the temporal ones), so it's easier to review. Then in the PK patch (now #2) we 
call that function to figure out the = and && operators. I think this is a big improvement.

I provide a couple "example" implementations:

- An identity function that returns whatever you give it. The core gist opclasses use this since 
they use the RT* constants. Even though not all opclasses support all strategies, it is okay to 
return a stratnum with no amop entry. You will just get an error when you try to make a temporal PK 
with that type as the WITHOUT OVERLAPS part (which is natural for the types we're talking about).

- A function that translates RT*StrategyNumbers to BT*StrategyNumbers when possible (just 
=/</<=/>/>=, and we really only need =). This is what the btree_gist opclasses use. (No other 
RT*StrategyNumber can be translated, which means you can only use these types for the non-WIHOUT 
OVERLAPS part, but again that is natural.)

I didn't add a similar function to GIN. It's not possible to use GIN for temporal PKs, so I don't 
think it makes sense.

## Foreign Keys

For FKs, I need a couple similar things:

- The ContainedBy operator (<@ for rangetypes).
- An aggregate function to combine referenced rows (instead of hardcoding range_agg as before).

I look up ContainedBy just as I'm doing with Equal & Overlap for PKs. The aggregate function is 
another optional support function.

I broke out that support function into another independent patch here. Then I updated by FKs patch 
to use it (and the ContainedBy operator).

## FOR PORTION OF

Then for FOR PORTION OF I need an intersect operator (*) and a new "leftovers" operator.

We have an intersect operator (for range & multirange at least), but no strategy number for it, thus 
no amop entry. My patch adds that, **but** it is neither a search operator ('s') nor ordering ('o'), 
so I've added a "portion" option ('p'). I'm not sure this is completely valid, since `FOR PORTION 
OF` is not really an *index* operation, but it does seem index-adjacent: you would only/usually use 
it on something with a temporal PK (which is an index). And it is an analogous situation, where 
pg_amop entries tell us how to implement the extensible parts. But if all this seems like the wrong 
approach please let me know.

The new leftovers operator similarly has 'p' for amoppurpose and another amop entry.

The leftovers operator returns an array of T, where T is the type of the valid_at column. Then we'll 
insert a new "leftovers" row for each array entry. So we aren't assuming only "before" and "after" 
(which doesn't work for multiranges or two-dimensional spaces as you'd have with bitemporal or spatial).

But now that "leftovers" are becoming more of an external-facing part of Postgres, I feel we should 
have a less silly name. (That's too bad, because "leftovers" is memorable and non-ambiguous, and 
computer pioneers used all kinds of silly names, so if you tell me I don't have to be quite so 
professional, maybe I'll go back to it.) I considered things like "without" or "multi-subtract" or 
"except". I went with "without portion", which is nice because it evokes FOR PORTION OF and doesn't 
limit the scope to rangetypes.

For the symbol I like `@-`. It conveys the similarity to subtraction, and "@" can be a mnemonic for 
"array". (Too bad we can't use `--`, ha ha.) I also thought about `@-@`, but that is used already by 
path_length and lseg_length, and maybe a non-commutative operator deserves a non-palindromic name.

The {multi,}range_without_portion procs & operators are broken out into a separate commit, and the 
FPO patch now uses them in the exec node. It always made me a little uneasy to have rangetype code 
in nodeModifyTable.c, and now it's gone.

Then the last thing I need for FOR PORTION OF is a "constructor". In SQL:2011 you use `FOR PORTION 
OF valid_at FROM '2000-01-01' TO '2010-01-01'`. But FROM and TO don't really work for non-range 
types. So I added an alternate syntax that is `FOR PORTION OF valid_at 
(tsmultirange(tsrange('2001-01-01', '2002-02-02'), tsrange('2003-03-03', '2004-04-04')))`. In other 
words parens wrapping a value of the type you're using. I still support FROM & TO for building a 
range type, so we follow the standard.

That's it for now. Multiranges should be fully supported (but need lots more tests), as well as 
custom types. I've updated some of the docs, but I need to go through them and clarify where things 
don't necessarily have to be ranges.

Rebased to cb44a8345e.

Yours,

From a3785526f6fe2f4a8a7b53db12f75a15b16cdf56 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 13 Dec 2023 21:17:53 -0800
Subject: [PATCH v20 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                  |  3 +-
 contrib/btree_gist/btree_gist--1.7--1.8.sql  | 87 ++++++++++++++++++++
 contrib/btree_gist/btree_gist.control        |  2 +-
 doc/src/sgml/gist.sgml                       | 66 ++++++++++++++-
 doc/src/sgml/xindex.sgml                     |  8 +-
 src/backend/access/gist/gistutil.c           | 42 ++++++++++
 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              | 10 +++
 src/test/regress/expected/misc_functions.out | 25 ++++++
 src/test/regress/sql/misc_functions.sql      |  6 ++
 12 files changed, 272 insertions(+), 6 deletions(-)
 create mode 100644 contrib/btree_gist/btree_gist--1.7--1.8.sql

diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile
index 073dcc745c..fa1b987b39 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -33,7 +33,8 @@ 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 \
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 0000000000..12c2e6c14b
--- /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
+
+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) ;
+
+-- added in 1.2:
+ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD
+	FUNCTION  12 (timetz, timetz) gist_stratnum_btree (int2) ;
+
+-- added in 1.3:
+ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD
+	FUNCTION  12 (uuid, uuid) gist_stratnum_btree (int2) ;
+
+-- added in 1.4:
+ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD
+	FUNCTION  12 (macaddr8, macaddr8) gist_stratnum_btree (int2) ;
+
+-- added in 1.5:
+ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD
+	FUNCTION  12 (anyenum, anyenum) gist_stratnum_btree (int2) ;
+
+-- added in 1.7:
+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.control b/contrib/btree_gist/btree_gist.control
index fa9171a80a..abf66538f3 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/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 9ac6b03e6e..baa26c65cc 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,66 @@ 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>
+       Two translation functions are 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.
+       <literal>gist_stratnum_btree</literal> is for opclasses that
+       use the <literal>BT*StrategyNumber</literal> constants
+       (like those in <literal>btree_gist</literal>).
+       It returns <literal>BTEqualStrategyNumber</literal> for equals,
+       and <literal>InvalidStrategy</literal> for everything else.
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 22d8ad1aac..8c4ede3105 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 9ce3687dbf..fb8c626441 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,44 @@ 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);
+}
+
+/*
+ * 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/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 4c711ecfa8..4d1b2c4400 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 0235716c06..1816352b94 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 4c70da41de..85d3b876ae 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 9052f5262a..1787afba74 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12133,4 +12133,14 @@
   proargnames => '{tli,start_lsn,end_lsn,relfilenode,reltablespace,reldatabase,relforknumber,relblocknumber,is_limit_block}',
   prosrc => 'pg_wal_summary_contents' },
 
+# GiST stratnum implementations
+{ oid => '8047', descr => 'GiST support',
+  proname => 'gist_stratnum_identity', prorettype => 'int2',
+  proargtypes => 'int2',
+  prosrc => 'gist_stratnum_identity' },
+{ oid => '8048', descr => 'GiST support',
+  proname => 'gist_stratnum_btree', prorettype => 'int2',
+  proargtypes => 'int2',
+  prosrc => 'gist_stratnum_btree' },
+
 ]
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 9302134077..547af894aa 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -670,3 +670,28 @@ 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)
+
+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/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index d3dc591173..4a9a9cfa5d 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -250,3 +250,9 @@ 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);
+SELECT gist_stratnum_btree(3::smallint);
+SELECT gist_stratnum_btree(18::smallint);
-- 
2.42.0

From 6ffb7db7f01217cfe98c3ec3a5078d400a190ecc Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v20 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                   |   3 +-
 .../btree_gist/expected/without_overlaps.out  |  31 ++
 contrib/btree_gist/sql/without_overlaps.sql   |  13 +
 doc/src/sgml/catalogs.sgml                    |  10 +
 doc/src/sgml/gist.sgml                        |  18 +-
 doc/src/sgml/ref/create_table.sgml            |  39 +-
 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             |  14 +-
 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_private.h             |   1 +
 src/include/access/stratnum.h                 |   1 +
 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, 1131 insertions(+), 56 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 fa1b987b39..62aefadd81 100644
--- a/contrib/btree_gist/Makefile
+++ b/contrib/btree_gist/Makefile
@@ -39,7 +39,8 @@ PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes"

 
 
+     <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 baa26c65cc..f05875d36c 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

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

      <para>
 
      <para>
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index fb8c626441..78fe360556 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -1098,3 +1098,32 @@ gist_stratnum_btree(PG_FUNCTION_ARGS)
 			PG_RETURN_UINT16(InvalidStrategy);
 	}
 }
+
+/*
+ * 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 b93894889d..1e55b3419c 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2143,6 +2143,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);
@@ -2193,6 +2194,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
index 7b186c0220..14c2032876 100644

 
 	/*
 
 	if (conkeyArray)
index e56205abd8..8c06aa0956 100644
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "access/amapi.h"
+#include "access/gist_private.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,

+	/*
@@ -845,7 +856,7 @@ DefineIndex(Oid tableId,
 
@@ -860,7 +871,7 @@ DefineIndex(Oid tableId,
@@ -913,8 +924,9 @@ DefineIndex(Oid tableId,
 
 	/*
@@ -932,7 +944,7 @@ DefineIndex(Oid tableId,
@@ -986,10 +998,10 @@ DefineIndex(Oid tableId,
@@ -1028,12 +1040,12 @@ DefineIndex(Oid tableId,
 
-						if (stmt->unique)
 
@@ -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;

@@ -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";
+			}
+			GetOperatorFromCanonicalStrategy(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;
 }
 
+/*
+ * GetOperatorFromCanonicalStrategy
+ *
+ * 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 retrun InvalidStrategy.
+ */
+void
+GetOperatorFromCanonicalStrategy(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 (!StrategyIsValid(*strat))
+			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 6b0a20010e..d1189d74a8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10090,6 +10090,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  conislocal,	/* islocal */
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
+									  false,	/* conWithoutOverlaps */
 									  false);	/* is_internal */

 			/*

 
 
 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
index 63f172e175..02ada43a90 100644
 
+opt_without_overlaps:
index cf0d432ab1..a5bb45df1e 100644

 
+		/*
@@ -2673,6 +2682,23 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
 
 	/*

 

 

 
 
 	return nKeys;
 }

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..63fc7c4880 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7014,6 +7014,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
+				i_conwithoutoverlaps,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7095,10 +7096,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)

 	/*
@@ -7166,6 +7174,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7273,6 +7282,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -16978,6 +16988,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conwithoutoverlaps)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
index 673ca5c92d..10605ecac5 100644
@@ -488,6 +488,7 @@ typedef struct _constraintInfo
index 1639bdd9fc..6613425d63 100644

-					/* 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_private.h b/src/include/access/gist_private.h
index 82eb7b4bd8..28273fb986 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -532,6 +532,7 @@ extern void gistMakeUnionKey(GISTSTATE *giststate, int attno,
 							 Datum *dst, bool *dstisnull);
 
 extern XLogRecPtr gistGetFakeLSN(Relation rel);
+extern StrategyNumber gistTranslateStratnum(Oid opclass, StrategyNumber strat);
 
 /* gistvacuum.c */
 extern IndexBulkDeleteResult *gistbulkdelete(IndexVacuumInfo *info,
diff --git a/src/include/access/stratnum.h b/src/include/access/stratnum.h
index 1b29653a8a..7845d54012 100644
--- a/src/include/access/stratnum.h
+++ b/src/include/access/stratnum.h
@@ -22,6 +22,7 @@
 typedef uint16 StrategyNumber;
 
 #define InvalidStrategy ((StrategyNumber) 0)
+#define StrategyIsValid(stratnum) ((bool) ((stratnum) != InvalidStrategy))
 
 /*
  * Strategy numbers for B-tree indexes.
index 41a0b9aee1..739d843a2b 100644

+	/*
 
 	/*
 
 	/*

index 179eb9901f..1f738b0129 100644
@@ -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 GetOperatorFromCanonicalStrategy(Oid opclass, Oid atttype,
+											 const char *opname, Oid *opid,
+											 StrategyNumber *strat);
 
 /* commands/functioncmds.c */
 extern ObjectAddress CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt);
 
index 0000000000..cd0446fa51
@@ -0,0 +1,395 @@
+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 a multirange:
+CREATE TABLE temporal_mltrng (
+  valid_at tsmultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+\d temporal_mltrng
+              Table "public.temporal_mltrng"
+----------+--------------+-----------+----------+---------
+ valid_at | tsmultirange |           | not null | 
+    "temporal_mltrng_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.
 
index 0000000000..39ea24f643
@@ -0,0 +1,290 @@
+-- PK with a multirange:
+CREATE TABLE temporal_mltrng (
+  valid_at tsmultirange,
+  CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+\d temporal_mltrng

From 5a427185c8b887ad82902fb7c78291c5137aad2f Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 29 Dec 2023 14:36:27 -0800
Subject: [PATCH v20 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 f05875d36c..c4909e6d8a 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>
@@ -1249,6 +1253,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 8c4ede3105..3ee1361717 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 4d1b2c4400..abfcde6cba 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 5c2d77e908..8eb99e9b1b 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 1816352b94..a9c4ea9643 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -39,7 +39,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 85d3b876ae..ae0abbe38c 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 c8b31830f6a8716fd46c2ab5130c925d1048761c Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sun, 27 Jun 2021 12:58:51 -0700
Subject: [PATCH v20 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  |  27 +
 contrib/btree_gist/sql/without_overlaps.sql   |  12 +
 doc/src/sgml/ref/create_table.sgml            |  48 +-
 src/backend/commands/indexcmds.c              |  26 +-
 src/backend/commands/tablecmds.c              | 864 ++++++++++++------
 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     | 551 +++++++++++
 src/test/regress/sql/without_overlaps.sql     | 526 +++++++++++
 20 files changed, 2245 insertions(+), 353 deletions(-)

+-- Foreign key

 
@@ -1260,6 +1291,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 8c06aa0956..8d48b79cea 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2189,8 +2189,9 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				opname = "equals";
 			}
 			GetOperatorFromCanonicalStrategy(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)
  * GetOperatorFromCanonicalStrategy
  *
  * 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
  *
@@ -2445,11 +2447,14 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * function to translate from the well-known number
  * to the internal value. If the function isn't defined
  * or it gives no result, we retrun InvalidStrategy.
+ *
+ * rhstype can be InvalidOid to use the opcintype instead.
  */
 void
 GetOperatorFromCanonicalStrategy(Oid opclass,
-								 Oid atttype,
+								 Oid rhstype,
 								 const char *opname,
+								 const char *context,
 								 Oid *opid,
 								 StrategyNumber *strat)
 {
@@ -2473,12 +2478,19 @@ GetOperatorFromCanonicalStrategy(Oid opclass,
 		if (!StrategyIsValid(*strat))
 			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 +2512,11 @@ GetOperatorFromCanonicalStrategy(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 d1189d74a8..4320c6f15e 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_private.h"
 #include "access/heapam.h"
 #include "access/heapam_xlog.h"
 #include "access/multixact.h"
@@ -207,8 +208,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;
 
@@ -379,17 +383,20 @@ static int	transformColumnNameList(Oid relId, List *colList,
@@ -499,7 +506,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -509,7 +517,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -541,6 +551,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
@@ -5895,7 +5911,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
@@ -9524,6 +9541,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
+	Oid			periodoperoid;
+	Oid			periodprocoid;
@@ -9618,6 +9642,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9636,7 +9669,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9644,8 +9679,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9704,189 +9753,40 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
+	/*
+	 * 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.
@@ -9903,7 +9803,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
@@ -9919,7 +9820,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10004,7 +9906,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10090,7 +9993,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10166,7 +10069,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10224,7 +10128,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10272,6 +10177,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10389,7 +10295,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10420,7 +10326,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10656,7 +10563,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10749,6 +10657,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10864,6 +10773,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,
@@ -10895,7 +10805,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 */
@@ -10929,13 +10839,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
+	StrategyNumber	rtstrategy;
+	char	   *stratname;
+			stratname = "overlaps";
+			rtstrategy = RTOverlapStrategyNumber;
+			stratname = "equality";
+			rtstrategy = RTEqualStrategyNumber;
+		eqstrategy = gistTranslateStratnum(opclass, rtstrategy);
+		if (!StrategyIsValid(eqstrategy))
+					 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.")));
+ * FindFkPeriodOpersAndProcs -
+ * Looks up the oper and proc oids for confkperiodoperoids and confkperiodprocoids.
+ * These are used by foreign keys with a PERIOD element.
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *periodoperoid,
+						  Oid *periodprocoid)
+	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))
+				 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.
+	GetOperatorFromCanonicalStrategy(opclass,
+									 aggrettype,
+									 "contained by",
+									 "FOREIGN KEY constraint",
+									 periodoperoid,
@@ -11433,7 +11630,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11680,7 +11881,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11746,7 +11946,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;
@@ -11811,36 +12013,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11867,6 +12088,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11888,12 +12113,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11931,6 +12160,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12040,7 +12282,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12069,8 +12312,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;
 
 	/*
@@ -12139,6 +12384,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12158,12 +12404,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12221,37 +12473,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12281,37 +12564,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
index 02ada43a90..c563a5b79b 100644

 

+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);
@@ -17602,6 +17615,7 @@ reserved_keyword:
 			| ONLY
 			| OR
 			| ORDER
+			| PERIOD
 			| PLACING
 			| PRIMARY
 			| REFERENCES
@@ -17911,6 +17925,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 6945d99b3d..dec7b08644 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,
 
@@ -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)
 
 		/* ----------
 
+			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);
+		}
 
@@ -497,21 +547,49 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
 
+			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);
+		}
 
@@ -695,10 +782,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
 			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 -
+ *
 
 /*
  * RI_FKey_pk_upd_check_required -
@@ -1241,7 +1454,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
 		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;

@@ -1488,15 +1701,17 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
 
@@ -2137,6 +2352,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
@@ -2171,6 +2387,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 
 	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 -
  *
@@ -2801,7 +3033,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan)
@@ -2834,35 +3066,86 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 
 		if (rel_is_pk)
 		{
-			/*

 	return true;
 }
 
+/*
 
 /*
  * ri_AttributesEqual -
@@ -3021,12 +3304,49 @@ RI_FKey_trigger_type(Oid tgfoid)

 	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 25ac0ad1bc..1bf3aeb342 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,
-										 bool withoutOverlaps, StringInfo buf);
+										 bool withoutOverlaps, 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);

 

 

 
index fc6d267e44..12aede5da9 100644
@@ -1627,6 +1627,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 c17b1e9d31..0cc15a50d7 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];
 
 	/*
 
 	/*
 
 	/*

 
index 1787afba74..874ca98ed6 100644
 
index 1f738b0129..e2b9dca1e0 100644
@@ -50,8 +50,10 @@ extern bool CheckIndexCompatible(Oid oldId,
-extern void GetOperatorFromCanonicalStrategy(Oid opclass, Oid atttype,
-											 const char *opname, Oid *opid,
+extern void GetOperatorFromCanonicalStrategy(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 a9c6825601..82a1444d34 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 098990dd16..03c5e707b9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2563,6 +2563,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)
@@ -2609,7 +2613,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 5984dcfa4b..ccf5bf8aa6 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 c22cabdf42..e82fb6f425 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -120,6 +120,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 cd0446fa51..7e5e209237 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -393,3 +393,554 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
index 39ea24f643..9b79bb6345 100644
@@ -288,3 +288,529 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;

From 2ae071622b419286261a518d493e1a05d5ed525b Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v20 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.
---
 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 ++
 9 files changed, 394 insertions(+)

diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c
index 9443c2b884..0906345159 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 24bad52923..4eda39676b 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 b2cdea66c4..9b783ce326 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 => '8405', 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 874ca98ed6..727eb35087 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 => '8404',
+  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 6b420a8618..e1b51a1017 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 a0cb875492..297aa0cc85 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 ee02ff0163..1f14e96802 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 fefb4b4d42..659272791b 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 c23be928c3..562d46c5da 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 00aad8f25e4d2a5884d239326068f86547902b4e Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v20 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.
---
 doc/src/sgml/catalogs.sgml                    |  19 +-
 doc/src/sgml/ref/delete.sgml                  |  70 +++
 doc/src/sgml/ref/update.sgml                  |  88 ++++
 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        | 199 +++++++-
 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                  | 203 +++++++-
 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/tcop/utility.c                    |   2 +-
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/period.c                |  56 +++
 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/include/utils/period.h                    |  19 +
 src/test/regress/expected/for_portion_of.out  | 450 ++++++++++++++++++
 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       | 349 ++++++++++++++
 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 +++-
 52 files changed, 2278 insertions(+), 71 deletions(-)
 create mode 100644 src/backend/utils/adt/period.c
 create mode 100644 src/include/utils/period.h
 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/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 618a05ac89..46170c0749 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>

+  <para>
 
+   <varlistentry>
index 2ab24b0523..c51aef2175 100644
+    [ FOR PORTION OF <replaceable class="parameter">range_or_period_name</replaceable> <replaceable class="parameter">for_portion_of_target</replaceable> ]
 
+  <para>
@@ -114,6 +151,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 a5390ff644..538a1e078d 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>
 
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index abfcde6cba..1f05b0914f 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 4320c6f15e..b54e0e7406 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12355,6 +12355,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

 

 

 
 
+/* ----------
+ * ForPortionOfState()
+ *
 
index ee7e666171..bf563bbe8d 100644
@@ -58,9 +58,12 @@
 #include "rewrite/rewriteHandler.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
+#include "utils/array.h"

@@ -1205,6 +1212,115 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+/* ----------------------------------------------------------------
+ *		set_leftover_tuple_bounds
+ *
+ *		Saves the new bounds into the range attribute
+ * ----------------------------------------------------------------
+ */
+static void set_leftover_tuple_bounds(TupleTableSlot *leftoverTuple,
+									  ForPortionOfExpr *forPortionOf,
+									  TypeCacheEntry *typcache,
+									  Datum leftover)
+{
+	/* Store the range directly */
+
+	leftoverTuple->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
+	leftoverTuple->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+}
+
+/* ----------------------------------------------------------------
+ *		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)
+{
+	ModifyTableState *mtstate = context->mtstate;
+	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+	ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+	Datum	oldRange;
+	Datum	allLeftovers;
+	Datum	*leftovers;
+	int		nleftovers;
+	bool isNull = false;
+	TypeCacheEntry *typcache;
+	ForPortionOfState *fpoState = resultRelInfo->ri_forPortionOf;
+	TupleTableSlot *oldtupleSlot = fpoState->fp_Existing;
+	TupleTableSlot *leftoverTuple = fpoState->fp_Leftover;
+
+	/*
+	 * 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");
+
+	oldRange = slot_getattr(oldtupleSlot, forPortionOf->rangeVar->varattno, &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);
+
+	if (nleftovers > 0)
+	{
+		bool shouldFree;
+		HeapTuple oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, &shouldFree);
+		int i;
+		// TupleDesc tupDesc = resultRelInfo->ri_RelationDesc->rd_att;
+
+		for (i = 0; i < nleftovers; i++)
+		{
+			// TODO: okay to keep re-using the same leftoverTuple TTS every iteration?:
+			// leftoverTuple = ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual);
+			ExecForceStoreHeapTuple(oldtuple, leftoverTuple, false);
+
+			set_leftover_tuple_bounds(leftoverTuple, forPortionOf, typcache, leftovers[i]);
+			ExecMaterializeSlot(leftoverTuple);
+
+			// TODO: Need to save context->mtstate->mt_transition_capture? (See comment on ExecInsert)
+			ExecInsert(context, resultRelInfo, leftoverTuple, node->canSetTag, NULL, NULL);
+		}
+
+		if (shouldFree)
+			heap_freetuple(oldtuple);
+	}
+}
+
 /* ----------------------------------------------------------------
  *		ExecBatchInsert
  *
@@ -1357,7 +1473,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 +1507,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
@@ -2139,6 +2261,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -3630,6 +3757,7 @@ ExecModifyTable(PlanState *pstate)
@@ -4303,6 +4431,75 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+		/* Create the tuple slot for INSERTing the leftovers */
+		fpoState->fp_Leftover =
+		 * TODO: This is wrong if a column was dropped then added again!
+		 * We should use execute_attr_map_slot to use the correct attnums.

index 6f45efde21..c4b703a5a3 100644

 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 7a1dfb6364..5fea174ad2 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 @@
 
@@ -479,6 +489,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -512,6 +536,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -550,7 +575,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -1173,7 +1202,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1232,149 @@ 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;
+
+	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->range_name_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->range_name_location)));
+
+	rangeVar = makeVar(
+			rtindex,
+			range_attno,
+			attr->atttypid,
+			attr->atttypmod,
+			attr->attcollation,
+			0);
+	rangeVar->location = forPortionOf->range_name_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->range_name_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;
+	GetOperatorFromCanonicalStrategy(opclass, InvalidOid, "overlaps", "FOR PORTION OF", &opid, &strat);
+	result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
+			(Node *) copyObject(rangeVar), targetExpr,
+			forPortionOf->range_name_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;
+	GetOperatorFromCanonicalStrategy(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 *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;
+
+	result->range_name = range_name;
+
+	return result;
+}
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2581,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2426,6 +2599,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2442,7 +2619,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2451,7 +2629,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2470,7 +2648,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2520,6 +2698,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index c563a5b79b..a8904b2736 100644

 
 	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;
 				}
@@ -13732,6 +13749,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->range_name_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->range_name_location = @4;
+					n->target_start = $6;
+					n->target_end = $8;
+					$$ = n;
+				}
+			| /*EMPTY*/					{ $$ = NULL; }
+		;
+
 /*
  * TABLESAMPLE decoration in a FROM item
  */
@@ -17325,6 +17363,7 @@ unreserved_keyword:
 			| PASSWORD
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -17929,6 +17968,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 9bbad33fbd..bff10c9c78 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;
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
index 366a27ae8e..2a2982195e 100644
@@ -1556,7 +1556,7 @@ ProcessUtilitySlow(ParseState *pstate,
 
 					/*
index dec7b08644..1a2b9cf683 100644
@@ -230,6 +230,7 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -240,6 +241,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 static void lookupTRIOperAndProc(const RI_ConstraintInfo *riinfo,
 								 char **opname, char **aggname);

 /*
@@ -456,6 +462,7 @@ RI_FKey_check(TriggerData *trigdata)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					NULL, newslot,
+					-1, 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, 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 = 0;
 
@@ -815,9 +825,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -924,6 +941,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
 	ri_PerformCheck(riinfo, &qkey, qplan,
 					fk_rel, pk_rel,
 					oldslot, NULL,
+					-1, 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, 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, 0,
 					true,		/* must detect new rows */
 					SPI_OK_UPDATE);
 
@@ -2551,6 +2571,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2606,6 +2627,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -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);
index 12aede5da9..b8813042d0 100644
@@ -2201,6 +2201,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 7845d54012..d3fdf8d8e7 100644
--- a/src/include/access/stratnum.h
+++ b/src/include/access/stratnum.h
@@ -79,8 +79,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 c4d6adcd3e..baaf58f3e1 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 c9bb7b4787..f8b94af6ff 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 */

 /*
index 5d7f17dee0..b9402d29d4 100644
 
 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
+ *
+	TypeCacheEntry *fp_leftoverstypcache;	/* type cache entry of the range */
+	TupleTableSlot *fp_Leftover;		/* slot to store leftover */
@@ -541,6 +564,9 @@ typedef struct ResultRelInfo

index 03c5e707b9..d130445800 100644
 
@@ -1530,6 +1533,21 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
+ *		representation of FOR PORTION OF <period-name> FROM <ts> TO <te>
+ *		or FOR PORTION OF <period-name> (<target>)
+ */
+	Node	   *target;
@@ -1893,12 +1911,13 @@ typedef struct InsertStmt
 
@@ -1907,13 +1926,14 @@ typedef struct DeleteStmt
 
index bb930afb52..c618e3d551 100644
@@ -2068,4 +2068,30 @@ typedef struct OnConflictExpr
 
+/*----------
+	Oid			withoutPortionProc;	/* proc for old_range @- target_range */
 
index e82fb6f425..4b52e2dfde 100644
@@ -144,6 +144,8 @@ extern char get_rel_persistence(Oid relid);
index 0000000000..5b44a1a4dd
@@ -0,0 +1,450 @@
+-- With a direct target
+FOR PORTION OF valid_at (tsrange('2018-03-10', '2018-03-17'))
+SET name = 'one^5'
+ [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)
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7610b011d6..da079cfc58 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
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 7e5e209237..e73eb8bb33 100644
@@ -305,6 +305,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
@@ -357,6 +387,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -392,6 +452,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -682,13 +772,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -719,13 +821,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -747,9 +861,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
@@ -771,9 +898,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
 
 # ----------
index 0000000000..97ac5776f8
@@ -0,0 +1,349 @@
+-- With a direct target
+FOR PORTION OF valid_at (tsrange('2018-03-10', '2018-03-17'))
+SET name = 'one^5'
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index 2fe7b6dcc4..2dffe7c5b6 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

index 9b79bb6345..751c42ab6b 100644
@@ -231,6 +231,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
 
 --
@@ -269,6 +283,22 @@ INSERT INTO temporal_partitioned VALUES
 
 -- temporal UNIQUE:
@@ -287,6 +317,22 @@ INSERT INTO temporal_partitioned VALUES
 
 --
@@ -547,13 +593,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 
@@ -582,13 +638,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -608,14 +674,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 
@@ -632,9 +708,20 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-

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

---

index b54e0e7406..43cef5eef5 100644
@@ -508,7 +508,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -9655,7 +9655,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9838,12 +9838,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 
@@ -9853,6 +9856,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12490,11 +12500,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12581,11 +12599,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 1a2b9cf683..f944665d71 100644
@@ -84,6 +84,12 @@
 
@@ -193,6 +199,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
@@ -1410,6 +1417,127 @@ TRI_FKey_restrict_del(PG_FUNCTION_ARGS)
 
+/*
@@ -1447,6 +1575,415 @@ TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
 
+/*
@@ -2583,8 +3120,8 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -2619,8 +3156,10 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -3331,8 +3870,14 @@ RI_FKey_trigger_type(Oid tgfoid)
index 727eb35087..ca07766baf 100644
index e73eb8bb33..43a57d1d04 100644
@@ -917,30 +917,184 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
@@ -948,7 +1102,461 @@ ALTER TABLE temporal_fk_rng2rng
@@ -956,8 +1564,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -970,8 +1578,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -1006,7 +1614,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1018,7 +1626,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
@@ -1040,7 +1648,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1052,35 +1660,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
index 751c42ab6b..e40da0bc76 100644
@@ -726,31 +726,114 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');

 
@@ -758,6 +841,271 @@ ALTER TABLE temporal_fk_rng2rng

@@ -767,8 +1115,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -782,8 +1130,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -867,37 +1215,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 

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

index 46170c0749..6e8cf5e983 100644
 
+     <row>
@@ -5726,6 +5731,113 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
index 22d04006ad..8ea0bed42f 100644

 

 

+  <para>
 
index 17461a9189..c63f81f6b0 100644
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index 1e55b3419c..bb0e27bb9a 100644
@@ -2042,6 +2043,81 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 
+/*
index 893f73ecb5..398718052f 100644
@@ -1207,7 +1207,28 @@ GRANT SELECT ON parameters TO PUBLIC;
  * PERIODS view
  */

 /*
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case OCLASS_PERIOD:
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index e9a344d03e..21bae7ec3b 100644
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
index 43cef5eef5..19e1bd86c1 100644
@@ -44,6 +44,7 @@
@@ -150,13 +151,18 @@ static List *on_commits = NIL;
 
@@ -357,6 +363,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
@@ -438,6 +445,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
@@ -457,6 +466,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -665,6 +680,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -893,6 +912,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
@@ -1278,6 +1317,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1394,6 +1448,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*

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

 
 /*

 
+/*
 
+/*
@@ -13700,6 +14430,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15763,7 +16502,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index a8904b2736..9e3029e5c2 100644

+
 TypedTableElement:

+TablePeriod:

index a5bb45df1e..c02aa1b097 100644

+			case T_PeriodDef:

+void

 	/*
 
+		/*
@@ -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:
+				{
index b8813042d0..1497906053 100644

 /*
@@ -3564,6 +3627,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
 
 /*
index 63fc7c4880..7753830f7f 100644
@@ -6381,6 +6381,7 @@ getTables(Archive *fout, int *numTables)
@@ -6458,6 +6459,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6595,6 +6604,7 @@ getTables(Archive *fout, int *numTables)
@@ -6678,6 +6688,7 @@ getTables(Archive *fout, int *numTables)
@@ -8416,7 +8427,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8469,6 +8480,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8483,7 +8496,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9019,15 +9033,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9049,6 +9084,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9068,12 +9104,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9132,6 +9169,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10402,6 +10512,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -15937,6 +16049,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -15945,7 +16083,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16163,7 +16301,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16465,7 +16603,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18550,6 +18688,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 10605ecac5..917c700a57 100644
 
@@ -493,6 +497,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;

 
 
+		/* print periods */

 
 /*
index 82a1444d34..8854fd973c 100644

 
 
@@ -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 */
index d130445800..56c19beae1 100644
@@ -2141,6 +2141,7 @@ typedef enum ObjectType
@@ -2228,6 +2229,8 @@ typedef enum AlterTableType
@@ -2494,11 +2497,11 @@ typedef struct VariableShowStmt
 
@@ -2507,6 +2510,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 */
@@ -2521,6 +2525,30 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3232,6 +3260,7 @@ typedef struct IndexStmt
 
index 4b52e2dfde..2864610221 100644
@@ -198,6 +200,7 @@ extern Oid	get_range_subtype(Oid rangeOid);
 
 # ----------

---

From: Paul Jungwirth <[email-masked]>
Date: Mon, 1 Jan 2024 17:59:47 -0800

On 12/31/23 00:51, Paul Jungwirth wrote:
> That's it for now.

Here is another update. I fixed FOR PORTION OF on partitioned tables, in particular when the attnums 
are different from the root partition.

Rebased to cea89c93a1.

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

 
 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
 
 /*
 
 # gin

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

 

 
+     <row>
 
  <variablelist>
 
       <para>

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

      <para>
 
      <para>
 
 	pfree(ccbin);

 
 	/*
 
 	if (conkeyArray)

 
 
+	/*

 	/*
 
-						if (stmt->unique)

 	indexRelationId =

 
 		/*
 
+/*
index efd6078f8b..8d1658d26b 100644

 			/*

 
 
 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
 
+opt_without_overlaps:

 
+		/*
 
 	/*

 

 

 
 
 	return nKeys;
 }

 
 
 	/*

 

 
 /* gistvacuum.c */

 /*

+	/*
 
 	/*
 
 	/*

 

index 0000000000..726e94102b
+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]');
+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]');
 
index 0000000000..c8e8ab994a
+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]');
+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]');

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

 
  <variablelist>
 
   <para>
 
   <para>

 	va_start(ap, maxargs);
 
 /*
 
 # gin

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

 
+-- Foreign key

 

 	if (!OidIsValid(*opid))
 
 		ereport(ERROR,
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8d1658d26b..af491e4b58 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@
 
@@ -211,8 +212,11 @@ typedef struct NewConstraint
 
@@ -383,17 +387,20 @@ static int	transformColumnNameList(Oid relId, List *colList,
@@ -503,7 +510,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -513,7 +521,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -545,6 +555,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
@@ -5898,7 +5914,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
@@ -9527,6 +9544,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9621,6 +9645,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9639,7 +9672,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9647,8 +9682,22 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9707,189 +9756,40 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 
+	/*
@@ -9906,7 +9806,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9922,7 +9823,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10007,7 +9909,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10093,7 +9996,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10169,7 +10072,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10227,7 +10131,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10275,6 +10180,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10392,7 +10298,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10423,7 +10329,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10659,7 +10566,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10752,6 +10660,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10867,6 +10776,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
@@ -10898,7 +10808,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
 		/* Set up partition dependencies for the new constraint */
@@ -10932,13 +10842,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
@@ -11436,7 +11633,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11683,7 +11884,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11749,7 +11949,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;
@@ -11814,36 +12016,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -11870,6 +12091,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -11891,12 +12116,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -11934,6 +12163,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12043,7 +12285,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12072,8 +12315,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;
 
 	/*
@@ -12142,6 +12387,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12161,12 +12407,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12224,37 +12476,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12284,37 +12567,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;
 }
+
+/*

 

 

 
 
+/*
 
 	/*
 
 	/*
 
 	/*

 

 
 #endif							/* TABLECMDS_H */
 
index 726e94102b..34f0e7b3be 100644
 
index c8e8ab994a..e36268346d 100644

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

 
+/*

 Datum
 
 ]

+-- without_portion

+-- without_portion

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

 doc/src/sgml/ref/update.sgml                  |  88 +++
 doc/src/sgml/trigger.sgml                     |  60 +-
 src/backend/executor/nodeModifyTable.c        | 263 +++++++-
 src/backend/parser/analyze.c                  | 203 ++++++-
 src/backend/parser/gram.y                     |  56 +-
 src/test/regress/expected/for_portion_of.out  | 562 ++++++++++++++++++
 src/test/regress/expected/updatable_views.out |  32 +
 src/test/regress/sql/for_portion_of.sql       | 423 +++++++++++++
 src/test/regress/sql/without_overlaps.sql     |  93 ++-
 52 files changed, 2527 insertions(+), 72 deletions(-)

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

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

 
 
index af491e4b58..ad5dc61638 100644
@@ -12358,6 +12358,7 @@ validateForeignKeyConstraint(char *conname,

 

 

 
 
+/* ----------
+ * ForPortionOfState()
+ *
 
index ee7e666171..0c60f6dbce 100644

@@ -1205,6 +1212,131 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
+/* ----------------------------------------------------------------
+ *		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)
+{
+	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
+	 * (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");
+
+	/*
+	 * 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);
+
+	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 +1489,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 +1523,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 +1908,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 +2281,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 											   NULL, NIL,
 											   (updateCxt->updateIndexes == TU_Summarizing));
 
@@ -3630,6 +3777,7 @@ ExecModifyTable(PlanState *pstate)
@@ -4303,6 +4451,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 c03f4f23e2..62ccbf9d3c 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2442,6 +2442,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;
@@ -2582,6 +2590,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))
@@ -3425,6 +3435,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->rangeSet, fpo->rangeSet, List *);
+
+				return (Node *) newnode;
+			}
+			break;
 		case T_PartitionPruneStepOp:
 			{
 				PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node;
@@ -3603,6 +3626,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 34ca6d4ac2..954eb4da83 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);

 

 
 #include "postgres.h"

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

+static Node *

 

+/*
 
 /*

 

 
+		/*

 
 	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

 
 /*

+/*
+ * ForPortionOfState
+ *

 
 
+/*
+ * ForPortionOfClause

 
+/*----------
 
index 0000000000..4376bced4d
@@ -0,0 +1,562 @@
+-- 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_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]');
+  ('[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;
+ [1,2) | [01-01-2000,01-01-2010) | one
+ [5,6) | [01-01-2000,01-01-2010) | five
+-- 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'
+-- 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'
+-- 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]'
+-- 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]'
+-- 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)
+-------+-------------------------+---------
+ [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;
+-------+-------------------------+---------
+ [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
+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)

 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 34f0e7b3be..b47c5b3f3d 100644

 
 # ----------
index 0000000000..6010c5d94f
@@ -0,0 +1,423 @@
+-- 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_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]');
+  ('[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'
+-- 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'
+-- 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]'
+-- 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]'
+-- 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_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;

 
index e36268346d..780de0a5db 100644

 -- temporal UNIQUE:

 

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

---

index ad5dc61638..4cf18b7584 100644
@@ -512,7 +512,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -9658,7 +9658,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9841,12 +9841,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 
@@ -9856,6 +9859,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12493,11 +12503,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12584,11 +12602,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*
index b47c5b3f3d..61d4710b58 100644
+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]');
+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]');
index 780de0a5db..b82748dc1f 100644

 

@@ -767,8 +1115,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'),
@@ -782,8 +1130,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]');

 

 

 

 

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

 src/backend/commands/tablecmds.c              | 749 +++++++++++++++++-
 src/test/regress/expected/periods.out         | 177 +++++
 48 files changed, 2309 insertions(+), 37 deletions(-)

+     <row>

 

 

+  <para>

+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+

+/*

 /*
 
+		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 4cf18b7584..12092a8ec8 100644
@@ -152,6 +153,11 @@ typedef enum AlterTablePass
 	AT_PASS_OLD_CONSTR,			/* re-add existing constraints */
 	AT_PASS_ADD_COL,			/* ADD COLUMN */
+	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 */
@@ -361,6 +367,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
@@ -442,6 +449,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
@@ -461,6 +470,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -669,6 +684,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -897,6 +916,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
@@ -1282,6 +1321,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1398,6 +1452,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3351,6 +3663,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4407,12 +4881,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4421,7 +4895,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4514,6 +4988,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4829,6 +5305,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5225,6 +5709,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6369,6 +6861,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6392,6 +6886,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -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;
 }
 
+/*
@@ -8098,6 +8679,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -13703,6 +14433,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -15766,7 +16505,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*

+
 TypedTableElement:

+TablePeriod:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 

 

 
+		/* print periods */

 
 /*

 

 #endif							/* TABLECMDS_H */

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

 # ----------

---

From: jian he <[email-masked]>
Date: Fri, 5 Jan 2024 13:06:28 +0800

Hi.

+/*

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");
}

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

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

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

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.

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)

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

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".

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.

---

From: Paul Jungwirth <[email-masked]>
Date: Fri, 5 Jan 2024 16:19:53 -0800

Getting caught up on reviews from November and December:

 > CASCADE);  -- should be error?

Okay, I've added a restriction for temporal FKs too. But note this will
change once the PERIODs patch (the last one here) is finished. When the
generated column is for a PERIOD, there will be logic to "reroute" the
updates to the constituent start/end columns instead.

 > CONSTRAINT fk FOREIGN KEY (parent_id, valid_at)

Ah okay, thanks for the clarification! This is tricky because the user
left out the PERIOD on the fk side, and left out the entire pk side, so
those columns are just implicit. So there is no PERIOD anywhere.
But I agree that if the pk has WITHOUT OVERLAPS, we should expect a
corresponding PERIOD modifier on the fk side and explain that that's
what's missing. The attached patches include that.

 > I first do `git apply` then  `git diff --check`, there is a white

Fixed, thanks!

 > Since at most, it can return 'UPDATE 3' or 'DELETE 3'.

This doesn't sound correct to me. An UPDATE or DELETE can target many
rows. Also I don't think the inserted "leftovers" should be included in
these counts. They represent the rows updated/deleted.

 >      update pk for portion of a from lb to up set b = 'lb_to_up';

Hmm this is a tough one. It is correct that the `FROM __ TO __` values cannot be column references. 
They are computed up front, not per row. One reason is they are used to search the table. In fact 
the standard basically allows nothing but literal strings here. See section 14.14, page 971 then 
look up <point in time> on page 348 and <datetime value expression> on page 308. The most 
flexibility you get is you can add/subtract an interval to the datetime literal. We are already well 
past that by allowing expressions, (certain) functions, parameters, etc.

OTOH in your plpgsql example they are not really columns. They just get represented as ColumnRefs 
and then passed to transformColumnRef. I'm surprised plpgsql does it that way. As a workaround you 
could use `EXECUTE format(...)`, but I'd love to make that work as you show instead. I'll keep 
working on this one but it's not done yet. Perhaps I can move the restriction into 
analysis/planning. If anyone has any advice it'd be welcome.

 > based on comments on ExprContext. I refactor a bit, and solved this TODO.

The patch looks wrong to me. We need to range targeted by `FROM __
TO __` to live for the whole statement, not just one tuple (see just
above). That's why it gets computed in the Init function node.

I don't think that TODO is needed anymore at all. Older versions of the
patch had more expressions besides this one, and I think it was those I
was concerned about. So I've removed the comment here.

 > with "referencing old table as old_table new table as new_table"

Thank you for the test case! This is very helpful. So the problem is
`referencing new table as new_table` gets lost. I don't have a fix yet
but I'll work on it.

 > Is the above part unnecessary?

Good catch! Fixed.

 > better put it on in 0005?

You're right, those changes should have been in the PERIODs patch. Moved.

 > means "valid_at" will be interval range data type, which does not exist now.

It appears still needed to me. Without those lines I get 4 shift/reduce
conflicts. Are you seeing something different? Or if you have a better
solution I'd love to add it. I definitely need to fix this before that
patch gets applied.

 > for all the refactor related to ri_PerformCheck, do you need (Datum) 0

Casts added.

 > So "range" is more accurate than "interval"?

I don't think we should be using R to define the terms "range" and
"interval", which both already have meanings in Postgres, SQL, and the
literature for temporal databases. But I'm planning to revise the docs'
terminology here anyway. Some temporal database texts use "interval"
in this sense, and I thought it was a decent term to mean "range or
PERIOD". But now we need something to mean "range or multirange or
custom type or PERIOD". Actually "portion" seems like maybe the best
term, since the SQL syntax `FOR PORTION OF` reinforces that term. If you
have suggestions I'm happy for ideas.

You're right, the other function comments here use imperative mood. Changed.

New patches attached, rebased to 43b46aae12. I'll work on your feedback from Jan 4 next. Thanks!

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

index 073dcc745c4..fa1b987b39b 100644
 
index 00000000000..12c2e6c14b2
index fa9171a80a2..abf66538f32 100644
index 9ac6b03e6e4..baa26c65cc3 100644
 
  <para>
 
  <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>
index dddc08893a1..5bf13ec680e 100644
index 7e2a715200a..698e01ed2f7 100644
index c6dcd6a90dd..e7ced18a5ba 100644
 
 /*
index f639c3a6a51..352558c1f06 100644
 
 # gin
index 7979392776d..59b0a697496 100644
@@ -12143,4 +12143,14 @@
 
index 93021340773..547af894aaa 100644
 
index d3dc591173d..4a9a9cfa5d9 100644

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

index fa1b987b39b..62aefadd81e 100644

 
index 00000000000..069272688fb
index 00000000000..98665dbb9ec
index 3ec7391ec5e..618a05ac896 100644
 
+     <row>
index baa26c65cc3..f05875d36c1 100644
 
  <variablelist>
 
       <para>
index e04a0692c4e..a0bd2b8adc6 100644

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

      <para>
 
      <para>
index 5bf13ec680e..1056ddd6428 100644
index e80a90ef4c0..7abc2e0d22d 100644
 
 	pfree(ccbin);
index 88f7994b5a6..2934b553062 100644

 
 	/*
index a0232aa1bbf..0a95608179d 100644
 
 	if (conkeyArray)
index 340248a3f29..30162ee7aae 100644

 
 
+	/*

 	/*
 
-						if (stmt->unique)

 	indexRelationId =

 
 		/*
 
+/*
index 2822b2bb440..c696438ed5e 100644
@@ -10225,6 +10225,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10523,6 +10524,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
+									  false,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -11028,6 +11030,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								  false,	/* islocal */
 								  1,	/* inhcount */
 								  false,	/* conNoInherit */
+								  false,	/* conWithoutOverlaps */
 								  true);
 
@@ -14441,7 +14444,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 
index 6873c3b4d9d..34ea3d19c8c 100644
 
index 27cc186c16f..a400fb39f67 100644
index 296ba845187..03f67b68506 100644
index 1624b345812..cfb552fde74 100644
 
 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
index 6b88096e8e1..0a57611c383 100644
@@ -4127,7 +4127,7 @@ ConstraintElem:
@@ -4136,11 +4136,12 @@ ConstraintElem:
@@ -4161,7 +4162,7 @@ ConstraintElem:
@@ -4169,11 +4170,12 @@ ConstraintElem:
@@ -4241,6 +4243,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 
+opt_without_overlaps:
index b2d1fa9d0d6..0772a0210e7 100644

 
+		/*
 
 	/*
index 0b2a164057b..d140ab4bdda 100644

 

 

 
 
 	return nKeys;
 }
index 20273f0be16..66e19b9f6da 100644

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 22d1e6cf922..13da2453a11 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)

 	/*
@@ -7385,6 +7393,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7492,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
@@ -17200,6 +17210,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conwithoutoverlaps)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
index 9a34347cfc7..d1a6dc07232 100644
index 3912dbf481a..4a4d81c4d9f 100644
 
index 37f95163201..3f8bff59548 100644
 
index 7b8749c8db0..2af7fbacbac 100644

 /* gistvacuum.c */
index 8a47d3c9ec8..eaf3bb02764 100644

 /*
index 99dab5940bc..2ef8512dbff 100644
 
index d79432e142b..8fd76a94d78 100644
 
+	/*
 
 	/*
 
 	/*

index 29c511e3196..b25ff2a8773 100644

index b3181f34aee..476d55dd240 100644
@@ -2590,6 +2590,7 @@ typedef struct Constraint
 
@@ -3219,6 +3220,7 @@ typedef struct IndexStmt
index 00000000000..726e94102bf
index f0987ff537b..b08aaae44b8 100644
 
index 00000000000..c8e8ab994a0

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

index f05875d36c1..c4909e6d8af 100644
 
  <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>
index 698e01ed2f7..4bd2e531d71 100644
index 32bb477f328..71638cb401b 100644

 	va_start(ap, maxargs);
index e7ced18a5ba..51168cf9d70 100644
 
 /*
index 352558c1f06..1d3d5fcf4d8 100644
 
 # gin

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

 src/backend/commands/tablecmds.c              | 916 ++++++++++++------
 .../regress/expected/without_overlaps.out     | 561 +++++++++++
 src/test/regress/sql/without_overlaps.sql     | 535 ++++++++++
 20 files changed, 2294 insertions(+), 375 deletions(-)

index 069272688fb..d11601e570d 100644
 
+-- Foreign key
index 98665dbb9ec..036ee0a760f 100644
index a0bd2b8adc6..b5f48525ded 100644

 
index 30162ee7aae..aac4b72ad86 100644

 	if (!OidIsValid(*opid))
 
 		ereport(ERROR,
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c696438ed5e..ea069738e9d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@
 
@@ -212,8 +213,11 @@ typedef struct NewConstraint
 
@@ -384,17 +388,20 @@ static int	transformColumnNameList(Oid relId, List *colList,
@@ -506,7 +513,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -516,7 +524,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -548,6 +558,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
@@ -5912,7 +5928,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9659,6 +9676,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,
@@ -9753,6 +9777,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,
@@ -9771,16 +9806,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);
 	}
 
@@ -9789,34 +9846,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.
 	 *
@@ -9839,189 +9868,68 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
+		pkattnum[numpks] = pkperiodattnum;
+		pktypoid[numpks] = pkperiodtypoid;
+		fkattnum[numpks] = fkperiodattnum;
+		fktypoid[numpks] = fkperiodtypoid;
 
+				pkperiodtypoid, fkperiodtypoid, opclasses[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)
+						 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)
+						 errmsg("invalid %s action for foreign key constraint containing generated column",
+								"ON DELETE")));
 
+	/*
@@ -10038,7 +9946,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10054,7 +9963,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10139,7 +10049,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10225,7 +10136,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10301,7 +10212,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10359,7 +10271,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10407,6 +10320,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			tab->constraints = lappend(tab->constraints, newcon);
@@ -10524,7 +10438,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10555,7 +10469,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10791,7 +10706,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conwithoutoverlaps);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10884,6 +10800,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10999,6 +10916,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
@@ -11030,7 +10948,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
 		/* Set up partition dependencies for the new constraint */
@@ -11064,13 +10982,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
@@ -11568,7 +11773,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -11815,7 +12024,6 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	return address;
 }
 
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -11881,7 +12089,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;
@@ -11946,36 +12156,55 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -12002,6 +12231,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -12023,12 +12256,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -12066,6 +12303,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12175,7 +12425,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12204,8 +12455,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;
 
 	/*
@@ -12274,6 +12527,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_period;
 
 	/*
@@ -12293,12 +12547,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12356,37 +12616,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12416,37 +12707,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
index 03f67b68506..2adee6d5db2 100644
index cfb552fde74..bff835b4c18 100644
index 0a57611c383..58dcb47c218 100644

 
@@ -4216,21 +4217,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
@@ -4258,6 +4261,16 @@ columnList:
 
+optionalPeriodName:
@@ -17612,6 +17625,7 @@ reserved_keyword:
@@ -17921,6 +17935,7 @@ bare_label_keyword:
index 708d9c8efc9..0dadf2acb2d 100644

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

 

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

 

 
 
+	/*
 
 	/*

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

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

 	return RI_TRIGGER_NONE;
 }
+
+/*
index d140ab4bdda..5aedb188715 100644

 

 

 
index 8ec83561bfa..713ae61931c 100644
 
+/*
index 8fd76a94d78..75804d66ee7 100644
 
 	/*
 
 	/*
 
 	/*

 
index 59b0a697496..86fc7d27d79 100644
 
index b25ff2a8773..8b60562e26f 100644
 
index 60d9b8f5b5f..96769472bbb 100644
 
 #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'
 
@@ -2610,7 +2614,9 @@ typedef struct Constraint
index 2331acac091..1d3f7bd37f2 100644
index be9ed70e841..62c25691ead 100644
index 726e94102bf..c633738c2eb 100644
@@ -393,3 +393,564 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 (1 row)
 
+-- (parent_id, valid_at) REFERENCES [implicit]
index c8e8ab994a0..641cdd5b19f 100644
@@ -288,3 +288,538 @@ SELECT * FROM temporal_partitioned ORDER BY id, valid_at;
+-- (parent_id, valid_at) REFERENCES [implicit]

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

index f82e6f42d98..b705aee6be5 100644
 
+/*
index d99b00b5908..a0ec4682a5f 100644

 Datum
index 0e7511dde1c..d226c84aab3 100644
+{ oid => '8409', descr => 'range without portion',
 
 ]
index 86fc7d27d79..2b36ced5505 100644
+{ oid => '8408',
index 2b574873cef..e1b256b65f4 100644
 
index a0cb8754928..297aa0cc855 100644
 
+-- without_portion
index ee02ff01631..1f14e96802c 100644
 
index fefb4b4d422..659272791bf 100644
 
+-- without_portion
index c23be928c32..562d46c5da9 100644

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

 .../postgres_fdw/expected/postgres_fdw.out    |   5 +
 contrib/postgres_fdw/sql/postgres_fdw.sql     |   4 +
 src/backend/executor/nodeModifyTable.c        | 262 +++++++-
 src/backend/parser/analyze.c                  | 209 ++++++-
 51 files changed, 2483 insertions(+), 71 deletions(-)

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 618a05ac896..46170c07499 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 @@
 
index 1b81b4e7d74..389de5bc068 100644
 
+  <para>
 
+   <varlistentry>
index 2ab24b0523e..c51aef2175e 100644
 
+  <para>
 
+   <varlistentry>
index a5390ff6443..538a1e078dc 100644

index 4bd2e531d71..97c139a10d3 100644

index ea069738e9d..e878b41ecb0 100644
@@ -12498,6 +12498,7 @@ validateForeignKeyConstraint(char *conname,

index 34ea3d19c8c..59ee0fade19 100644

 

 

+/* ----------
+ * ForPortionOfState()
+ *
+ * Copys a ForPortionOfState into the current memory context.
+ */
 
index 13a9b7da83b..b2eb5bde736 100644
index 9fc5abff8f0..96c18eab992 100644

 

@@ -4303,6 +4450,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index 030463cb429..a2e3350d1c3 100644
index ca619eab944..41f5f8d2f7b 100644

index 667723b6753..0443793d3ac 100644
 
index f7ac71087a6..535b5150a41 100644

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 06fc8ce98b5..f8cd3c11bfd 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 *

 
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1232,155 @@ transformOnConflictClause(ParseState *pstate,
 	return result;
 }
 
+/*
+	/* We don't support FOR PORTION OF FDW queries. */
+	if (targetrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+				 errmsg("foreign tables don't support FOR PORTION OF")));
 
 /*
  * BuildOnConflictExcludedTargetlist
@@ -2409,6 +2587,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2426,6 +2605,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* 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;

@@ -2451,7 +2635,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2470,7 +2654,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2520,6 +2704,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index 58dcb47c218..28d27a7c3c1 100644

 
 	QUOTE
@@ -12225,14 +12238,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;
 				}
@@ -12295,6 +12310,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
@@ -12303,10 +12319,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;
 				}
@@ -13742,6 +13759,27 @@ relation_expr_opt_alias: relation_expr					%prec UMINUS
 				}
 		;
 
+for_portion_of_clause:
@@ -17335,6 +17373,7 @@ unreserved_keyword:
@@ -17939,6 +17978,7 @@ bare_label_keyword:
index 7b211a7743f..15b5c0dedc1 100644
 
 			/*
 
 			/*
index 44529bb49e6..14ffb7ba649 100644
index 9300c7b9abc..f3c49de8099 100644
 
 			/*
 
 			/*
index fdb3e6df338..fc17dd7edf8 100644
 
 			/*
index 5f6a683ab94..0733616f036 100644
index 5e5c1c9e2d3..e5902581086 100644
 
index 0dadf2acb2d..4d17b3877bd 100644

 /*
+					-1, (Datum) 0,
 
+							 -1, (Datum) 0,
 
@@ -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;

+					-1, (Datum) 0,
 
+					-1, (Datum) 0,
 
+					-1, (Datum) 0,

 	/*
index 713ae61931c..bb197d954b6 100644
 
+/*
+ * get_typname
+ *
index eaf3bb02764..4b98243c377 100644

 #endif							/* STRATNUM_H */
index d8a05214b11..a565d337dec 100644
 
 # gist point_ops
 
 # GiST multirange_ops
 
 # btree multirange_ops
index 8ea18a2fe1b..f561a11f7fb 100644

index 8a5a9fe6422..3d156483325 100644
 
 /*
index 561fdd98f1b..2dcb7379a21 100644

+/*
+ * ForPortionOfState
+ *

index 8f4f8ce6a77..c9368311239 100644

+/*
+ * ForPortionOfClause

index bb34cfb843f..33944b74b3f 100644
index b4ef6bc44c9..9cb9b18a4c1 100644
index 4a154606d2b..859bb860b69 100644
 
+/*----------
index 03a0e46e706..62575552560 100644
index 5ecdd109466..e512f4b24ea 100644
 
index 1d3f7bd37f2..707a517339a 100644
index 99d65157365..5e740fef881 100644
index 62c25691ead..21436d5a401 100644
index 00000000000..0a8af3edb01
index 00000000000..4376bced4db
index 7610b011d68..da079cfc58a 100644
 
index fbb0489a4ff..3395d2150ff 100644
index a73c1f90c4a..727ad08e082 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index c633738c2eb..60c9eedaee8 100644

@@ -692,13 +782,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -729,13 +831,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -757,9 +871,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
@@ -781,9 +908,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
index b08aaae44b8..cc2f6b46e0c 100644
 
 # ----------
index 00000000000..6010c5d94f5
index 2fe7b6dcc49..2dffe7c5b6a 100644
 
index 3f68cafcd1e..e10added9d9 100644
 
index eaee0b7e1d7..f814701a43a 100644
 
index 641cdd5b19f..95b6da4c076 100644

 -- temporal UNIQUE:
 
 --
@@ -556,13 +602,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 
@@ -591,13 +647,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -617,14 +683,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 
@@ -641,9 +717,20 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-

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

---

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index e878b41ecb0..14226632de4 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 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,
@@ -9792,7 +9792,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);
 
@@ -9981,12 +9981,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;
 
@@ -9996,6 +9999,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12633,11 +12643,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12724,11 +12742,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
index 4d17b3877bd..001f6ed4d7e 100644

+/*
 
+/*
 
 	/*
index 2b36ced5505..5cb4b26f620 100644
index 60c9eedaee8..4f8b08c4ce3 100644
@@ -927,30 +927,184 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
@@ -958,7 +1112,461 @@ ALTER TABLE temporal_fk_rng2rng
@@ -966,8 +1574,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -980,8 +1588,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -1016,7 +1624,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1028,7 +1636,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
@@ -1050,7 +1658,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
@@ -1062,35 +1670,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-01-
index 95b6da4c076..8aeb245cbe9 100644
@@ -735,31 +735,114 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');

 
@@ -767,6 +850,271 @@ ALTER TABLE temporal_fk_rng2rng

@@ -776,8 +1124,8 @@ CREATE TABLE temporal_partitioned_rng (
   name text,
@@ -791,8 +1139,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -876,37 +1224,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 

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

 50 files changed, 2366 insertions(+), 37 deletions(-)

index 46170c07499..6e8cf5e983a 100644
 
+     <row>
index 22d04006ade..8ea0bed42f6 100644

index 0ca7d5a9e0b..56a4464ab75 100644

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index eaada230248..c061dcc0a3a 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>
@@ -583,6 +585,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index 5b43c56b133..49c2df99449 100644
index b5f48525ded..b00bd80cf58 100644

 
+  <para>
 
index d6b23b4bfd5..b3b519d9e4c 100644
index 590affb79a5..50ca706a160 100644
index e742c78ea35..3e1d2cbc692 100644
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index 7abc2e0d22d..32a50a7b3c6 100644
 
+/*
index c402ae72741..50ca0d05f87 100644

 /*
index 2f3ded8a0e7..a372fb0aae8 100644
index 8bb2924b9cd..8597b0cc2ae 100644
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
 
+		case OCLASS_PERIOD:
 
+		case OCLASS_PERIOD:
+			{
+				HeapTuple	perTup;
+				Form_pg_period per;
+
index 00000000000..465b5b675e3
index 80c40eaf578..2c1d1a10a7c 100644
index b2e4260aef7..3467b68c0c6 100644
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
index e9d50fc7d87..05fefb5b5ce 100644
index 87a2db4e0f5..4ae9921d3cb 100644
index b146d2aaf44..cf5aa5386a0 100644
index 5607273bf9f..af7177179ce 100644
index 14226632de4..f01b122046d 100644
@@ -153,6 +154,11 @@ typedef enum AlterTablePass
 	AT_PASS_OLD_INDEX,			/* re-add existing indexes */
@@ -362,6 +368,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
@@ -443,6 +450,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
@@ -462,6 +471,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,
@@ -674,6 +689,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -902,6 +921,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
@@ -1287,6 +1326,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1403,6 +1457,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3356,6 +3668,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4412,12 +4886,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4426,7 +4900,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4519,6 +4993,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4835,6 +5311,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5236,6 +5720,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6383,6 +6875,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6408,6 +6902,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7413,14 +7909,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7464,6 +7975,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
@@ -8115,6 +8696,155 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
@@ -14055,6 +14785,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 
+			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)
@@ -15931,7 +16670,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

index dce898c751c..54bdb4bdeaf 100644

 
 
 		/*
index a2e3350d1c3..201117ead9d 100644
index 28d27a7c3c1..6802481fc38 100644
 
@@ -2624,6 +2624,24 @@ alter_table_cmd:
@@ -3743,8 +3761,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4096,6 +4116,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7154,6 +7187,14 @@ CommentStmt:
index 34a0ec59019..17e85a7c83b 100644

index 0772a0210e7..84df1d32192 100644

+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{
index 199eae525d1..d04c75b3980 100644
index 00000000000..0ed4304e16d
index bb197d954b6..73a9d0a3486 100644

 /*
 
+Oid
 
 /*
index c10a7b3d0d2..efd8450baef 100644
index 256d1e35a4e..0a2696c5597 100644
index 13da2453a11..0162fc4fbe2 100644
@@ -6600,6 +6600,7 @@ getTables(Archive *fout, int *numTables)
@@ -6677,6 +6678,14 @@ getTables(Archive *fout, int *numTables)
 
@@ -6814,6 +6823,7 @@ getTables(Archive *fout, int *numTables)
@@ -6897,6 +6907,7 @@ getTables(Archive *fout, int *numTables)
@@ -8635,7 +8646,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8688,6 +8699,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8702,7 +8715,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9238,15 +9252,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9268,6 +9303,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9287,12 +9323,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9351,6 +9388,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10621,6 +10731,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16159,6 +16271,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16167,7 +16305,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16385,7 +16523,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16687,7 +16825,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18772,6 +18910,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 d1a6dc07232..bff9ee897bd 100644
@@ -300,12 +301,14 @@ typedef struct _tableInfo
@@ -355,6 +358,7 @@ typedef struct _tableInfo
 
@@ -494,6 +498,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;
 
index f358dd22b9d..b25b3862ab2 100644
@@ -120,6 +121,7 @@ static const int dbObjectTypePriority[] =
@@ -1370,6 +1372,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
index 3f8bff59548..61c69f11ed9 100644

 
+		/* print periods */
index fd588659014..e84d90c0e5e 100644
index 1d7f8380d90..3592a444bd1 100644
 
index 2bcbe327cfe..5596c523b7e 100644
index e4f3f708071..b4ccf2fa69e 100644
index 00000000000..d2cffa07bcd
index a1f527130bf..d97a95bd2fb 100644

 /*
index 96769472bbb..89e538c7504 100644

 

 #endif							/* TABLECMDS_H */
index c9368311239..cebfc7a4545 100644
@@ -2229,6 +2230,8 @@ typedef enum AlterTableType
@@ -2495,11 +2498,11 @@ typedef struct VariableShowStmt
 
@@ -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;
 
+
+/* ----------
@@ -3233,6 +3261,7 @@ typedef struct IndexStmt
index 14065894779..d84d02b0280 100644
 
index 21436d5a401..96f1fd7e83e 100644
index 3fb3314b6ca..c942e867386 100644
index 48563b2cf01..a954e703384 100644
@@ -183,6 +183,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
index 00000000000..6481c9b3056
index cc2f6b46e0c..d6b8b96d88e 100644
 
 # ----------
index 00000000000..f0be8902179

---

From: jian he <[email-masked]>
Date: Mon, 8 Jan 2024 22:54:00 +0800

> "range_not_overlap" would be better.

range_intersect returns the intersection of two ranges.
I think here we are doing the opposite.
names the main SQL function "range_not_intersect" and the internal
function as "range_not_intersect_internal" should be fine.
so people don't need to understand the meaning of "portion".

---

From: Paul Jungwirth <[email-masked]>
Date: Mon, 8 Jan 2024 10:54:13 -0800

> so people don't need to understand the meaning of "portion".

Thank you for helping me figure out a name here! I realize that can be a bike-sheddy kind of 
discussion, so let me share some of my principles.

Range and multirange are highly mathematically "pure", and that's something I value in them. It 
makes them more general-purpose, less encumbered by edge cases, easier to combine, and easier to 
reason about. Preserving that close connection to math is a big goal.

What I've called `without_portion` is (like) a closed form of minus (hence `@-` for the operator). 
Minus isn't closed under everything (e.g. ranges), so `without_portion` adds arrays---much as to 
close subtraction we add negative numbers and to close division we add rationals). We get the same 
effect from multiranges, but that only buys us range support. It would be awesome to support 
arbitrary types: ranges, multiranges, mdranges, boxes, polygons, inets, etc., so I think an array is 
the way to go here. And then each array element is a "leftover". What do we call a closed form of 
minus that returns arrays?

Using "not" suggests a function that returns true/false, but `@-` returns an array of things. So 
instead of "not" let's consider "complement". I think that's what you're expressing re intersection.

But `@-` is not the same as the complement of intersection. For one thing, `@-` is not commutative. 
`old_range @- target_portion` is not the same as `target_portion @- old_range`. But 
`complement(old_range * target_portion)` *is* the same as `complement(target_portion * old_range)`. 
Or from another angle: it's true that `old_range @- target_portion = old_range @- (old_range * 
target_portion)`, but the intersection isn't "doing" anything here. It's true that intersection and 
minus both "reduce" what you put in, but minus is more accurate.

So I think we want a name that captures that idea of "minus". Both "not" and "intersection" are 
misleading IMO.

Of course "minus" is already taken (and you wouldn't expect it to return arrays anyway), which is 
why I'm thinking about names like "without" or "except". Or maybe "multi-minus". I still think 
"without portion" is the closest to capturing everything above (and avoids ambiguity with other SQL 
operations). And the "portion" ties the operator to `FOR PORTION OF`, which is its purpose. But I 
wouldn't be surprised if there were something better.

Yours,

---

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

+/*
+ * 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)

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

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

maybe change the comment to
"Insert tuples for the not intersection of a row in a FOR PORTION OF
UPDATE/DELETE."

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

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

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.
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?

---

From: vignesh C <[email-masked]>
Date: Tue, 9 Jan 2024 12:16:34 +0530

> New patches attached, rebased to 43b46aae12. I'll work on your feedback from Jan 4 next. Thanks!

One of the test has failed in CFBot at [1] with:

diff -U3 /tmp/cirrus-ci-build/src/test/regress/expected/generated.out
/tmp/cirrus-ci-build/src/test/recovery/tmp_check/results/generated.out
--- /tmp/cirrus-ci-build/src/test/regress/expected/generated.out
2024-01-06 00:34:48.078691251 +0000
+++ /tmp/cirrus-ci-build/src/test/recovery/tmp_check/results/generated.out
2024-01-06 00:42:08.782292390 +0000
@@ -19,7 +19,9 @@
  table_name | column_name | dependent_column
 ------------+-------------+------------------
  gtest1     | a           | b
-(1 row)
+ pt         | de          | p
+ pt         | ds          | p
+(3 rows)

More details of the failure is available at [2].

[1] - https://cirrus-ci.com/task/5739983420522496
[2] - https://api.cirrus-ci.com/v1/artifact/task/5739983420522496/log/src/test/recovery/tmp_check/log/regress_log_027_stream_regress

Regards,
Vignesh

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 11 Jan 2024 15:44:50 +0100

On 31.12.23 09:51, Paul Jungwirth wrote:
> Here is a patch series with the GiST stratnum support function added. I 
> put this into a separate patch (before all the temporal ones), so it's 
> easier to review. Then in the PK patch (now #2) we call that function to 
> figure out the = and && operators. I think this is a big improvement.

I like this solution.

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.

* 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?

Is there a reason for the extra space after FUNCTION here (repeated
throughout the file)?

+-- added in 1.4:

What is the purpose of these "added in" comments?

v20-0002-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch

* contrib/btree_gist/Makefile

Also update meson.build.

* contrib/btree_gist/sql/without_overlaps.sql

Maybe also insert a few values, to verify that the constraint actually
does something?

* 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?

* 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?

* 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.

Also, most other functions there appear to be named "GistSomething",
so a more consistent name might be GistTranslateStratnum.

* src/include/access/stratnum.h

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

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?

If these points are addressed, and maybe with another round of checking 
that all corner cases are covered, I think these patches (0001 and 0002) 
are close to ready.

---

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

> Needs corresponding meson.build updates.

fixed

> Is there a reason for the extra space after FUNCTION here (repeated

fixed.

> * contrib/btree_gist/Makefile

fixed.

> Maybe also insert a few values, to verify that the constraint actually

I added an ok and failed INSERT.

> the " WITHOUT OVERLAPS" in the caller after returning from it?

fixed. i just print it right after decompile_column_index_array.

> one caller for the new function.  I suggest to do without it.

If more StrategyNumber are used in the future, will StrategyIsValid()
make sense?

> stick with one.  Or explain the relationship?

In my words:
for range type, well-known strategy number and canonical strategy
number are the same thing.
For types Gist does not natively support equality, like int4,
GetOperatorFromCanonicalStrategy will pass RTEqualStrategyNumber from
ComputeIndexAttrs
and return BTEqualStrategyNumber.

> that all corner cases are covered, I think these patches (0001 and 0002)

the following are my review:

I am not sure the above comment is related to the code

+/*
the above comment seems not right?
even though currently strat will only be RTEqualStrategyNumber.

+void
+GetOperatorFromCanonicalStrategy(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 (!StrategyIsValid(*strat))
+ 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))));
+ }
+}
I refactored this function.
GetOperatorFromCanonicalStrategy called both for normal and WITHOUT OVERLAPS.
so errmsg("no %s operator found for WITHOUT OVERLAPS constraint",
opname) would be misleading
for columns without "WITHOUT OVERLAPS".
Also since that error part was deemed unreachable, it would make the
error verbose, I guess.

I don't know how to verify it.
I think it should be:
+ if (pset.sversion >= 170000)
+      appendPQExpBufferStr(&buf, ", con.conwithoutoverlaps");

I refactored the 0002 commit message.
The original commit message seems outdated.
I put all the related changes into one attachment.

---

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:

 >          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.)

 > "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(2.0, 3.0) @- numrange(1.1, 2.2);

Correct, @- is not commutative.

 > select oprname,oprkind,oprcanhash,oprcanmerge,oprleft,oprright,oprresult,oprcode

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

 > one into the result array.

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

 >            oprcode           | oprname |    oprleft

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.

 > zero for "portion operator" for the newly implemented "portion

Okay, done.

 > it's not related to that patch, it also makes people easy to review.

You're right, sorry!

 > (that's the pattern I found all over other code)

Agreed, done.

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

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

Changed to "untouched portion".

 > (assume only range not multirange) would improve readability.

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

 > please see the attached build sql-update.html.

Still TODO.

Fixed.

 > 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.)
 >

Fixed.

 > Are there other extensions that use the btree strategy numbers for

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.

Fixed.

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

Done.

Done.

 > strategy number mapping, any type with a supported operator class

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

 > the " WITHOUT OVERLAPS" in the caller after returning from it?

Okay, done.

 > gist.  So access/gist.h would be a better place for it.

Moved.

Changed.

 > * src/include/commands/defrem.h

Okay, removed.

 > 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 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

index 073dcc745c4..a4101448935 100644
@@ -33,12 +33,14 @@ EXTENSION = btree_gist
 
+        stratnum

index 00000000000..307bfe574b0
+CREATE FUNCTION gist_stratnum_btree(smallint)
+RETURNS smallint
+LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
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"

 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>
 
  <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>
 
   <para>
index dddc08893a1..d4d08bd118f 100644
@@ -1056,3 +1057,16 @@ gistGetFakeLSN(Relation rel)
 
 /*
 
 # 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)
 
index d3dc591173d..9e40e606859 100644
@@ -250,3 +250,7 @@ FROM pg_walfile_name_offset('0/0'::pg_lsn + :segment_size + 1),

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

 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/ref/create_table.sgml            |  43 +-
 src/backend/utils/adt/ruleutils.c             |   2 +
 src/include/access/gist.h                     |   3 +
 33 files changed, 1155 insertions(+), 50 deletions(-)

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

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>
index 913c5cd3bba..86be7c96256 100644
 
  <variablelist>
 
       <para>
index e04a0692c4e..278af2e3869 100644

     <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>
 
      <para>
@@ -1019,7 +1045,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1052,9 +1078,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <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);
 }
+
+/*
+GistTranslateStratnum(Oid opclass, StrategyNumber strat)
index 45a71081d42..c73f7bcd011 100644
@@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 
 	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;

 	/* 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);
 
 	/*
 
 	if (conkeyArray)
index 340248a3f29..52f3194791d 100644
 
+#include "access/gist.h"

 
+	/*

 	/*
 
-						if (stmt->unique)

 	indexRelationId =

+			GetOperatorFromWellKnownStrategy(opclassOids[attn],
 
 		/*
 
+/*
+ * GetOperatorFromWellKnownStrategy
+ *
+ * or it gives no result, we return InvalidStrategy.
+GetOperatorFromWellKnownStrategy(Oid opclass,
+		*strat = GistTranslateStratnum(opclass, opstrat);
+		if (*strat == InvalidStrategy)
index 2a56a4357c9..e8622b7a8e5 100644
@@ -10336,6 +10336,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 	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);
 
@@ -14552,7 +14555,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
 
index 0880ca51fb2..a7c92416f56 100644

 		case CONSTR_PRIMARY:
 			READ_NODE_FIELD(keys);
index 3460fea56ba..130f7fc7c3f 100644
@@ -529,7 +529,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -4133,7 +4133,7 @@ ConstraintElem:
@@ -4142,11 +4142,12 @@ ConstraintElem:
@@ -4167,7 +4168,7 @@ ConstraintElem:
@@ -4175,11 +4176,12 @@ ConstraintElem:
@@ -4247,6 +4249,11 @@ opt_no_inherit:	NO INHERIT							{  $$ = true; }
 
+opt_without_overlaps:

 
+		/*
 
 	/*
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");

 
 
index bc20a025ce4..a19443becd6 100644

 	/*
 
@@ -17129,6 +17139,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
+			if (coninfo->conwithoutoverlaps)
+				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
index f0772d21579..f523c3a590d 100644
@@ -479,6 +479,7 @@ typedef struct _constraintInfo

index e7ced18a5ba..22dd04c1418 100644
@@ -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 */

+	/*
 
 	/*
 
 	/*

index 29c511e3196..4352a438899 100644
 
+extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,

 

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

index 86be7c96256..87948a77cfd 100644
 
  <variablelist>
@@ -1248,6 +1252,114 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
 
   <para>
 
   <para>

 	va_start(ap, maxargs);
index 22dd04c1418..641677e191c 100644
@@ -40,7 +40,8 @@
 
 /*
 
 # gin

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

 .../btree_gist/expected/without_overlaps.out  |  48 +
 contrib/btree_gist/sql/without_overlaps.sql   |  28 +
 src/backend/commands/indexcmds.c              |  24 +-
 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>
 
@@ -1162,11 +1162,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
@@ -1240,6 +1258,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -1251,6 +1275,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
@@ -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.")));

 	if (!OidIsValid(*opid))
@@ -2500,11 +2510,11 @@ GetOperatorFromWellKnownStrategy(Oid opclass,
 
 		ereport(ERROR,
 
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/gist.h"
 
@@ -507,7 +514,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -517,7 +525,9 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
@@ -549,6 +559,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
@@ -5931,7 +5947,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
@@ -9770,6 +9787,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
@@ -9864,6 +9888,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9882,16 +9917,38 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -9900,34 +9957,6 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	checkFkeyPermissions(pkrel, pkattnum, numpks);
 
-	/*
@@ -9950,189 +9979,68 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,

 

+	/*
@@ -10149,7 +10057,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10165,7 +10074,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10250,7 +10160,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10336,7 +10247,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10412,7 +10323,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10470,7 +10382,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10518,6 +10431,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			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));
 
@@ -11141,7 +11059,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 
 		/* 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);
 	}

+static void
+		eqstrategy = GistTranslateStratnum(opclass, rtstrategy);
+		if (eqstrategy == InvalidStrategy)
+	GetOperatorFromWellKnownStrategy(opclass,
@@ -11679,7 +11884,11 @@ ATExecAlterConstrRecurse(Constraint *cmdcon, Relation conrel, Relation tgrel,
 
 			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,
 
 	/*
 
-		attnums[i] = pkattno;

-	return i;
 
 /*
@@ -12113,6 +12342,10 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 	/*
@@ -12134,12 +12367,16 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
@@ -12177,6 +12414,19 @@ transformFkeyCheckAttrs(Relation pkrel,
 
 			/*
 			 * 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;
 
 	/*
@@ -12404,12 +12658,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 
@@ -12467,37 +12727,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
@@ -12527,37 +12818,68 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 
index 130f7fc7c3f..a1182042b86 100644
@@ -522,12 +522,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

 
@@ -4222,21 +4223,23 @@ ConstraintElem:
 								   NULL, yyscanner);
 					$$ = (Node *) n;
 				}
 
@@ -4264,6 +4267,16 @@ columnList:
 
+optionalPeriodName:
@@ -17618,6 +17631,7 @@ reserved_keyword:
@@ -17927,6 +17941,7 @@ bare_label_keyword:

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

 

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

 

 
 
+	/*
 
 	/*

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

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

 	return RI_TRIGGER_NONE;
 }
+
+/*
index b625f471a84..e896bd2bec7 100644
+										 bool withPeriod, StringInfo buf);

 

 
 
-				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;
 }
 
+/*
 
 	/*
 
 	/*
 
 	/*

 
index a0277e57c7d..14200460abb 100644
 
index 4352a438899..baeea439ee5 100644
-extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
+extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
+											 const char *opname,
+											 const char *context,
+											 Oid *opid,
 											 StrategyNumber *strat);

 #endif							/* TABLECMDS_H */
 
index e4a200b00ec..1339382f91d 100644
@@ -119,6 +119,7 @@ extern Oid	get_negator(Oid opno);

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

 doc/src/sgml/catalogs.sgml                    |   2 +-
 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);
 }
 
+/*

 Datum
 
 ]
index 14200460abb..8f95e082170 100644
 
index 9808587532c..a41a865cc1d 100644
 
+-- without_portion
index 07d5621ef87..f536e8f7aaa 100644
 
index cadf312031f..aa6f20d8adb 100644
 
+-- without_portion
index c5dbe0c04f1..64938ad082a 100644

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

 doc/src/sgml/ref/delete.sgml                  |  69 +++
 doc/src/sgml/ref/update.sgml                  |  87 +++
 src/backend/executor/nodeModifyTable.c        | 260 +++++++-
 50 files changed, 2460 insertions(+), 71 deletions(-)

 
index 0ca4ed6c2f8..1ac8b74fe3d 100644
 
   <table>
@@ -833,8 +835,9 @@
 
index 1b81b4e7d74..1c7275341ea 100644
@@ -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,
@@ -116,6 +150,41 @@ DELETE FROM [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ *
     </listitem>
    </varlistentry>
 
+   <varlistentry>
index 2ab24b0523e..71a02697f2d 100644
@@ -51,6 +52,41 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
    circumstances.
   </para>
 
+  <para>
+   If the table has a range or multirange column,
@@ -114,6 +150,57 @@ UPDATE [ ONLY ] <replaceable class="parameter">table_name</replaceable> [ * ] [
     </listitem>
    </varlistentry>
 
+   <varlistentry>

 
 
index cdef174ee24..ef7f58f99ac 100644
@@ -12609,6 +12609,7 @@ validateForeignKeyConstraint(char *conname,

index a7c92416f56..a0db2f01323 100644

@@ -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;

@@ -3641,6 +3648,7 @@ typedef struct AfterTriggerSharedData
@@ -3914,6 +3922,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc);

@@ -4123,6 +4132,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
@@ -4487,6 +4497,7 @@ AfterTriggerExecute(EState *estate,

@@ -6023,6 +6034,43 @@ AfterTriggerPendingOnRel(Oid relid)
 	return false;
 }
 
+/* ----------
+ * ForPortionOfState()
+ *
@@ -6438,6 +6486,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 
index 9fc5abff8f0..dfdcdbcde6d 100644
@@ -58,9 +58,11 @@

@@ -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;
 }
 
+ *		Insert tuples for the untouched portion of a row in a FOR
+	Assert(nleftovers >= 0);
@@ -1357,7 +1487,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1390,6 +1521,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1770,7 +1906,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2139,6 +2279,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4303,6 +4448,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index e1a5bc7e95d..6c19844e17e 100644
@@ -2444,6 +2444,14 @@ expression_tree_walker_impl(Node *node,
@@ -2584,6 +2592,8 @@ query_tree_walker_impl(Query *query,
@@ -3430,6 +3440,19 @@ expression_tree_mutator_impl(Node *node,
+				MUTATE(newnode->rangeTargetList, fpo->rangeTargetList, List *);
@@ -3608,6 +3631,7 @@ query_tree_mutator_impl(Query *query,

 
index 8dbf790e893..22bde21a362 100644
@@ -3708,7 +3708,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3774,6 +3775,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
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"

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

+static Node *

 

+/*
+				 parser_errposition(pstate, forPortionOf->location)));
+				 parser_errposition(pstate, forPortionOf->location)));
+	rangeVar->location = forPortionOf->location;
+				forPortionOf->location);
+	GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "overlaps", "FOR PORTION OF", &opid, &strat);
+			forPortionOf->location);
+	GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "without portion", "FOR PORTION OF", &opid, &strat);
+		Expr *rangeTLEExpr;
+		GetOperatorFromWellKnownStrategy(opclass, InvalidOid, "intersects", "FOR PORTION OF", &opid, &strat);
+		rangeTLEExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, get_opname(opid),
+				forPortionOf->location);
+		rangeTLEExpr = (Expr *) transformExpr(pstate, (Node *) rangeTLEExpr, EXPR_KIND_UPDATE_PORTION);
+		result->rangeTargetList = NIL;
+		tle = makeTargetEntry(rangeTLEExpr, range_attno, range_name, false);
+		result->rangeTargetList = lappend(result->rangeTargetList, tle);
+		result->rangeTargetList = NIL;
 
 /*

 

 
+		/*
index a1182042b86..c85b8b094ec 100644

 
 	QUOTE
@@ -865,6 +867,17 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
@@ -12231,14 +12244,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;
 				}
@@ -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:
+					n->location = @4;
+					n->location = @4;
@@ -17341,6 +17379,7 @@ unreserved_keyword:
@@ -17945,6 +17984,7 @@ bare_label_keyword:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
index f60b34deb64..4c4a5dd0727 100644
@@ -3476,6 +3476,30 @@ rewriteTargetView(Query *parsetree, Relation view)
 
+		foreach(lc, parsetree->forPortionOf->rangeTargetList)
@@ -3814,6 +3838,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
+				foreach(tl, parsetree->forPortionOf->rangeTargetList)

 /*

 

 

 	/*
index 6cd14d4bd12..9a24882f895 100644
@@ -2174,6 +2174,59 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
+ * get_typname
+ *
index 8a47d3c9ec8..e93c4d1f86c 100644
@@ -78,8 +78,10 @@ typedef uint16 StrategyNumber;

 #endif							/* STRATNUM_H */
 
 # gist point_ops
 
 # GiST multirange_ops
 
 # btree multirange_ops

 
 /*

+/*
+ * ForPortionOfState
+ *

index 8f4f8ce6a77..bdada6b4252 100644

+/*
+ * ForPortionOfClause
+	int			location;

index b9713ec9aa6..2591f8611d4 100644
index 4a154606d2b..ea6701d83ca 100644
 
+/*----------
+	List	   *rangeTargetList;	/* List of TargetEntrys to set the time column(s) */
index c43d97b48a6..2014cf23234 100644
 
index 1339382f91d..9487873ad48 100644
 
index 1950e6f281f..f4b88bcc38d 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 # ----------

 

 -- temporal UNIQUE:

 

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

---

index ef7f58f99ac..54b4527aa77 100644
@@ -516,7 +516,7 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
@@ -9903,7 +9903,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -10092,12 +10092,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
  */
 void
 
@@ -10107,6 +10110,13 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
 
 		if (!seen)
@@ -12744,11 +12754,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
@@ -12835,11 +12853,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr

+/*
 
+/*
 
 	/*
index 8f95e082170..d233b9861cd 100644

 

 

 

 

 

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

 doc/src/sgml/ref/delete.sgml                  |   3 +-
 doc/src/sgml/ref/update.sgml                  |   3 +-
 53 files changed, 2389 insertions(+), 39 deletions(-)

index 1ac8b74fe3d..5c045bce186 100644
 
+     <row>
index fc03a349f0f..7e5539599e0 100644
@@ -1375,6 +1375,64 @@ CREATE TABLE circles (
   </sect2>
  </sect1>

index 9e66be4e83d..558cc7b3f15 100644

index 9670671107e..eca9c3ba545 100644
@@ -585,6 +587,29 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
index 69081759d43..79bbd2402a0 100644

 
+  <para>
 
index 1c7275341ea..b388be7508c 100644
@@ -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
    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
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index c73f7bcd011..d02cf0da8fb 100644
@@ -2040,6 +2041,81 @@ 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 54b4527aa77..b6451336a1c 100644
 static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
 									   Node *def, LOCKMODE lockmode, bool recurse, bool recursing);
@@ -675,6 +690,10 @@ static List *GetParentedForeignKeyRefs(Relation partition);

 /* ----------------------------------------------------------------
@@ -903,6 +922,26 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 						stmt->partbound != NULL,
 						&old_constraints, &old_notnulls);
 
+	/*
@@ -1288,6 +1327,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 
+	/*
 
 	/*
@@ -1404,6 +1458,264 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
@@ -3369,6 +3681,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

+/*----------
+ * MergePeriods
@@ -4425,12 +4899,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode,
@@ -4439,7 +4913,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse)

 
 /*
@@ -4532,6 +5006,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -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;
 
@@ -7444,6 +7955,76 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
 
+/*
@@ -14166,6 +14896,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 
+			case OCLASS_PERIOD:
+				ereport(ERROR,
@@ -16042,7 +16781,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index 6c19844e17e..676ed85d203 100644
index c85b8b094ec..a0a6f5681e4 100644
 
@@ -2625,6 +2625,24 @@ alter_table_cmd:
@@ -3749,8 +3767,10 @@ TableElement:
 
+
 TypedTableElement:
 			columnOptions						{ $$ = $1; }
 			| TableConstraint					{ $$ = $1; }
@@ -4102,6 +4122,19 @@ TableLikeOption:
 		;

+TablePeriod:
@@ -7160,6 +7193,14 @@ CommentStmt:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{
index 9a24882f895..54404c54a71 100644
@@ -994,6 +995,68 @@ get_attoptions(Oid relid, int16 attnum)
 	return result;
 }

 /*
@@ -3537,6 +3600,30 @@ get_multirange_range(Oid multirangeOid)
 		return InvalidOid;
 }
 
+Oid
 
 /*
index a19443becd6..b13cf284827 100644
 
@@ -8561,7 +8572,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8614,6 +8625,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8628,7 +8641,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9167,15 +9181,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -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;
 
@@ -9216,12 +9252,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9280,6 +9317,79 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
@@ -10550,6 +10660,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16088,6 +16200,32 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 				}
 			}
 
+			/*
@@ -16096,7 +16234,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16314,7 +16452,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16616,7 +16754,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -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:
index f523c3a590d..f4635b12887 100644
 
@@ -484,6 +488,16 @@ typedef struct _constraintInfo
 	bool		separate;		/* true if must dump as separate item */
 } ConstraintInfo;

 
 
+		/* print periods */

 
 /*

 

 #endif							/* TABLECMDS_H */
index bdada6b4252..a6262559be2 100644

+
+/* ----------
 
index 9487873ad48..c3ad230ea65 100644
@@ -95,6 +95,8 @@ extern Oid	get_atttype(Oid relid, AttrNumber attnum);
 
 # ----------

---

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.

 > with "referencing old table as old_table new table as new_table"

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

@@ -160,6 +166,7 @@ static bool ExecMergeMatched(ModifyTableContext *context,
+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:
+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 = %',
+      (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);
+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:  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:  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:  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:  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:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-21-2018,01-01-2020)",one)
+NOTICE:  trigger = for_portion_of_test_delete_trig, TG_LEVEL = ROW, OLD table = ("[1,2)","[01-01-2018,01-01-2020)",one)
+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:  trigger = for_portion_of_test_insert_trig, TG_LEVEL = ROW, NEW table = ("[1,2)","[01-02-2018,01-01-2020)",one)
+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:  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:
+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 = %',
+      (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);
+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 +-
 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

 
 /*

 
+/*
@@ -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_
 
 \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: jian he <[email-masked]>
Date: Fri, 2 Feb 2024 13:53:52 +0800

> So people can view the patch with tests more easily.
I've attached a new patch that further simplified the tests. (scope
v24 patch's 0002 and 0003)
Please ignore previous email attachments.

I've only applied the v24, 0002, 0003.
seems in doc/src/sgml/ref/create_table.sgml
lack the explanation of `<replaceable
class="parameter">temporal_interval</replaceable>`

since foreign key ON {UPDATE | DELETE} {CASCADE,SET NULL,SET DEFAULT}
not yet supported,
v24-0003 create_table.sgml should reflect that.

+ /*
+ FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid,
&periodprocoid);

within the function ATAddForeignKeyConstraint, you called
FindFKPeriodOpersAndProcs,
but never used the computed outputs: periodoperoid, periodprocoid,
opclasses.
We validate these(periodoperoid, periodprocoid) at
lookupTRIOperAndProc, FindFKPeriodOpersAndProcs.
I'm not sure whether FindFKPeriodOpersAndProcs in
ATAddForeignKeyConstraint is necessary.

+ * 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.
"or the FK side is a subset."  is misleading, should it be something
like "or the FK side is a subset of X"?

I believe our style should be (with proper indent)
if (indexStruct->indisexclusion)
return i - 1;
else
return i;

in transformFkeyCheckAttrs

can be simplified:
{
found = false;
if (periodattnum == indexStruct->indkey.values[numattrs])
{
opclasses[numattrs] = indclass->values[numattrs];
found = true;
}
}

Also wondering, at the end of the function transformFkeyCheckAttrs `if
(!found)` part:
do we need another error message handle is_temporal is true?

@@ -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;
primary key can only one WITHOUT OVERLAPS,
so *operoids and *procoids
can be replaced with just
`operoids, procoids`.
Also these two elements in struct NewConstraint not used in v24, 0002, 0003.

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 12 Feb 2024 10:55:09 +0100

I have done a review of the temporal foreign key patches in this patch
series (0002 and 0003, v24).

The patch set needs a rebase across c85977d8fef.  I was able to do it
manually, but it's a bit tricky, so perhaps you can post a new set to
help future reviews.

(Also, the last (0007) patch has some compiler warnings and also
causes the pg_upgrade test to fail.  I didn't check this further, but
that's why the cfbot is all red.)

In summary, in principle, this all looks more or less correct to me.

As a general comment, we need to figure out the right terminology
"period" vs. "temporal", especially if we are going to commit these
features incrementally.  But I didn't look at this too hard here yet.

* v24-0002-Add-GiST-referencedagg-support-func.patch

Do we really need this level of generality?  Are there examples not
using ranges that would need a different aggregate function?  Maybe
something with geometry (points and lines)?  But it seems to me that
then we'd also need some equivalent to "without portion" support for
those types and a multirange equivalent (basically another gist
support function wrapped around the 0004 patch).

* v24-0003-Add-temporal-FOREIGN-KEYs.patch

- contrib/btree_gist/expected/without_overlaps.out
- contrib/btree_gist/sql/without_overlaps.sql

typo "exusts"

- doc/src/sgml/ref/create_table.sgml

This mentions FOR PORTION OF from a later patch.

It is not documented that SET NULL and SET DEFAULT are not supported,
even though that is added in a later patch.  (So this patch should say
that it's not supported, and then the later patch should remove that.)

- src/backend/commands/indexcmds.c

The changes to GetOperatorFromWellKnownStrategy() don't work for
message translations.  We had discussed a similar issue for this
function previously.  I think it's ok to leave the function as it was.
The additional context could be added with location pointers or
errcontext() maybe, but it doesn't seem that important for now.

- src/backend/commands/tablecmds.c

The changes in ATAddForeignKeyConstraint(), which are the meat of the
changes in this file, are very difficult to review in detail.  I tried
different git-diff options to get a sensible view, but it wasn't
helpful.  Do we need to do some separate refactoring here first?

The error message "action not supported for temporal foreign keys"
could be more detailed, mention the action.  Look for example how the
error for the generated columns is phrased.  (But note that for
generated columns, the actions are impossible to support, whereas here
it is just something not done yet.  So there should probably still be
different error codes.)

- src/backend/nodes/outfuncs.c
- src/backend/nodes/readfuncs.c

Perhaps you would like to review my patch 0001 in
<https://www.postgresql.org/message-id/[email-masked]>,
which removes the custom out/read functions for the Constraint node.
Then you could get rid of these changes.

- src/backend/utils/adt/ri_triggers.c

The added #include "catalog/pg_range.h" doesn't appear to be used for
anything.

Maybe we can avoid the added #include "commands/tablecmds.h" by
putting the common function in some appropriate lower-level module.

typo "PEROID"

Renaming of ri_KeysEqual() to ri_KeysStable() doesn't improve clarity,
I think.  I think we can leave the old name and add a comment (as you
have done).  There is a general understanding around this feature set
that "equal" sometimes means "contained" or something like that.

The function ri_RangeAttributeNeedsCheck() could be documented better.
It's bit terse and unclear.  From the code, it looks like it is used
instead of row equality checks.  Maybe a different function name would
be suitable.

Various unnecessary reformatting in RI_FKey_check().

When assembling the SQL commands, you need to be very careful about
fully quoting and schema-qualifying everything.  See for example
ri_GenerateQual().

Have you checked that the generated queries can use indexes and have
suitable performance?  Do you have example execution plans maybe?

- src/backend/utils/adt/ruleutils.c

This seems ok in principle, but it's kind of weird that the new
argument of decompile_column_index_array() is called "withPeriod"
(which seems appropriate seeing what it does), but what we are passing
in is conwithoutoverlaps.  Maybe we need to reconsider the naming of
the constraint column?  Sorry, I made you change it from "contemporal"
or something, didn't I?  Maybe "conperiod" would cover both meanings
better?

- src/backend/utils/cache/lsyscache.c

get_func_name_and_namespace(): This function would at least need some
identifier quoting.  There is only one caller (lookupTRIOperAndProc),
so let's just put this code inline there; it's not worth a separate
global function.  (Also, you could use psprintf() here to simplify
palloc() + snprintf().)

- src/include/catalog/pg_constraint.h

You are changing in several comments "equality" to "comparison".  I
suspect you effectively mean "equality or containment"?  Maybe
"comparison" is too subtle to convey that meaning?  Maybe be more
explicit.

You are changing a foreign key from DECLARE_ARRAY_FOREIGN_KEY to
DECLARE_ARRAY_FOREIGN_KEY_OPT.  Add a comment about it, like the one
just above has.

- src/include/catalog/pg_proc.dat

For the names of the trigger functions, maybe instead of

     TRI_FKey_check_ins

something like

     RI_FKey_period_check_ins

so that all RI trigger functions group under a common prefix.

On second thought, do we even need separate functions for this?
Looking at ri_triggers.c, the temporal and non-temporal functions are
the same, and all the differences are handled in the underlying
implementation functions.

- src/include/nodes/parsenodes.h

The constants FKCONSTR_PERIOD_OP_CONTAINED_BY and
FKCONSTR_PERIOD_PROC_REFERENCED_AGG could use more documentation here.

For the Constraint struct, don't we just need a bool field saying
"this is a period FK", and then we'd know that the last column is the
period?  Like we did for the primary keys (bool without_overlaps).

- src/include/parser/kwlist.h

For this patch, the keyword PERIOD can be unreserved.  But it
apparently will need to be reserved later for the patch that
introduces PERIOD columns.  Maybe it would make sense to leave it
unreserved for this patch and upgrade it in the later one.

---

From: jian he <[email-masked]>
Date: Wed, 14 Feb 2024 13:00:00 +0800

Hi
more minor issues.

opening curly brace should be the next line, also do you think it's
good idea to add following in the `if (is_temporal)` branch
`
Assert(OidIsValid(fkperiodtypoid) && OidIsValid(pkperiodtypoid));
Assert(OidIsValid(pkperiodattnum > 0 && fkperiodattnum > 0));
`

` if (is_temporal)` branch, you can set the FindFKComparisonOperators
10th argument (is_temporal)
to true, since you are already in the ` if (is_temporal)` branch.

maybe we need some extra comments on
`
+ numfks += 1;
+ numpks += 1;
`
since it might not be that evident?

Do you think it's a good idea to list arguments line by line (with
good indentation) is good format? like:
FindFKComparisonOperators(fkconstraint,
tab,
i,
fkattnum,
&old_check_ok,
&old_pfeqop_item,
pktypoid[i],
fktypoid[i],
opclasses[i],
false,
false,
&pfeqoperators[i],
&ppeqoperators[i],
&ffeqoperators[i]);

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 29 Feb 2024 13:16:56 -0800

Hello,

Here is another patch series for application time. It addresses the feedback from the last few 
emails. Details below:

Fixed.

 > range column", maybe we can add it on another patch.

Rewrote this section to be clearer.

 > Please ignore previous email attachments.

Thanks, I've pulled in most of these changes to the tests.

 > class="parameter">temporal_interval</replaceable>`

You're right. Actually I think it is clearer without adding a separate name here, so I've updated 
the docs to use `column_name | period_name`.

 > v24-0003 create_table.sgml should reflect that.

Updated.

 > but never used the computed outputs: periodoperoid, periodprocoid, opclasses.

This is explained in the comment above: we will do the same lookup when the foreign key is checked, 
but we should make sure it works now so we can report the problem to the user.

Okay, changed.

Fixed.

Changed.

 > do we need another error message handle is_temporal is true?

I think the existing error message works well for both temporal and non-temporal cases.

 > Also these two elements in struct NewConstraint not used in v24, 0002, 0003.

I've removed these entirely. Sorry, they were leftover from an earlier revision.

 > that's why the cfbot is all red.)

Fixed the pg_upgrade problem. I'm not seeing compiler warnings. If they still exist can you point me 
to those?

 > features incrementally.  But I didn't look at this too hard here yet.

Agreed. I think it is okay to use "temporal" in the docs for the feature in general, if we clarify 
that non-temporal values are also supported. That is what the rest of the world calls this kind of 
thing.

The word "period" is confusing because it can be the `PERIOD` keyword used in temporal FKs, or also 
the SQL:2011 `PERIOD` object that is like our range types. And then we also have ranges, etc. In the 
past I was using "interval" to mean "range or PERIOD" (and "interval" is used by Date in his 
temporal book), but perhaps that is too idiosyncratic. I've removed "interval" from the FK docs, and 
instead I've tried to be very explicit and avoid ambiguity. (I haven't given as much attention to 
cleaning up the later patches' docs yet.)

 > support function wrapped around the 0004 patch).

I'm not sure how else to do it. The issue is that `range_agg` returns a multirange, so the result 
type doesn't match the inputs. But other types will likely have the same problem: to combine boxes 
you may need a multibox. The combine mdranges you may need a multimdrange.

I agree we need something to support "without portion" too. The patches here give implementations 
for ranges and multiranges. But that is for `FOR PORTION OF`, so it comes after the foreign key 
patches (part 5 here).

Btw that part changed a bit since v24 because as jian he pointed out, our type system doesn't 
support anyrange inputs and an anyrange[] output. So I changed the support funcs to use SETOF. I 
could alternately add anyrangearray and anymultirangearray pseudotypes. It's not the first time I've 
wanted those, so I'd be happy to go that way if folks are open to it. It seems like it should be a 
totally separate patch though.

Fixed.

 > that it's not supported, and then the later patch should remove that.)

All fixed.

 > errcontext() maybe, but it doesn't seem that important for now.

Okay I've tried a different approach here that should fit better with t9n. Let me know if it still 
needs work.

 > helpful.  Do we need to do some separate refactoring here first?

I moved the FindFKComparisonOperators refactor into a separate patch, and that seems to confuse git 
less. Your suggestion to group the PERIOD attribute with the others (below) also helped a lot to cut 
down the diff here. In fact it means I only call FindFKComparisonOperators once, so pulling it into 
a separate method is not even necessary anymore. But I do think it helps simplify what's already a 
very long function, so I've left it in. Let me know if more work is needed here.

Fixed.

 > Then you could get rid of these changes.

That is a nice improvement!

 > The added #include "catalog/pg_range.h" doesn't appear to be used for

Removed.

 > putting the common function in some appropriate lower-level module.

Moved to pg_constraint.{c,h}.

Fixed.

Okay.

 > instead of row equality checks.  Maybe a different function name would

I realized I could simplify this a lot and reuse ri_AttributesEqual, so the whole method is gone now.

 > Various unnecessary reformatting in RI_FKey_check().

Fixed, sorry about that.

 > fully quoting and schema-qualifying everything.  See for example

Went through everything and added quoting & schemes to a few places that were missing it.

 > suitable performance?  Do you have example execution plans maybe?

The plans look good to me. Here are some tests:

-- test when inserting/updating the FK side:

regression=# explain analyze select 1
from (
select valid_at as r
from only temporal_rng x
where id = '[8,8]'
and valid_at && '[2010-01-01,2012-01-01)'
for key share of x
) x1
having '[2010-01-01,2012-01-01)'::tsrange <@ range_agg(x1.r);
                                                                     QUERY PLAN 

---------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate  (cost=8.19..8.20 rows=1 width=4) (actual time=0.165..0.167 rows=0 loops=1)
    Filter: ('["2010-01-01 00:00:00","2012-01-01 00:00:00")'::tsrange <@ range_agg(x1.r))
    Rows Removed by Filter: 1
    ->  Subquery Scan on x1  (cost=0.14..8.18 rows=1 width=32) (actual time=0.152..0.153 rows=0 loops=1)
          ->  LockRows  (cost=0.14..8.17 rows=1 width=38) (actual time=0.151..0.151 rows=0 loops=1)
                ->  Index Scan using temporal_rng_pk on temporal_rng x  (cost=0.14..8.16 rows=1 
width=38) (actual time=0.150..0.150 rows=0 loops=1)
                      Index Cond: ((id = '[8,9)'::int4range) AND (valid_at && '["2010-01-01 
00:00:00","2012-01-01 00:00:00")'::tsrange))
  Planning Time: 0.369 ms
  Execution Time: 0.289 ms
(9 rows)

-- test when deleting/updating from the PK side:

regression=# explain analyze select 1 from only temporal_rng x where id = '[8,8]' and valid_at && 
'[2010-01-01,2012-01-01)'
for key share of x;
                                                               QUERY PLAN 

---------------------------------------------------------------------------------------------------------------------------------------
  LockRows  (cost=0.14..8.17 rows=1 width=10) (actual time=0.079..0.079 rows=0 loops=1)
    ->  Index Scan using temporal_rng_pk on temporal_rng x  (cost=0.14..8.16 rows=1 width=10) 
(actual time=0.078..0.078 rows=0 loops=1)
          Index Cond: ((id = '[8,9)'::int4range) AND (valid_at && '["2010-01-01 
00:00:00","2012-01-01 00:00:00")'::tsrange))
  Planning Time: 0.249 ms
  Execution Time: 0.123 ms
(5 rows)

I will do some further tests with more rows, but I haven't yet.

 > or something, didn't I?  Maybe "conperiod" would cover both meanings

Certainly conperiod is easier to read. Since we are using it for PK/UNIQUE/FKs, conperiod also seems 
like a better match. FKs don't use WITHOUT OVERLAPS syntax, and OTOH PK/UNIQUEs will still accept a 
PERIOD (eventually, also a range/etc now). I've renamed it, but since the old name was already 
committed with the PK patch, I've broken the renaming into a separate patch that could be committed 
without anything else.

 > global function.  (Also, you could use psprintf() here to simplify

Removed.

Okay, changed.

 > DECLARE_ARRAY_FOREIGN_KEY_OPT.  Add a comment about it, like the one

I don't need this change at all now that we're using GENERATED columns for PERIODs, so I've taken it 
out.

 > so that all RI trigger functions group under a common prefix.

Renamed.

 > implementation functions.

My thinking was to avoid making the non-temporal functions suffer in performance and complexity. 
What do you think? I've kept the separate functions here but I can combine them if you like.

 > FKCONSTR_PERIOD_PROC_REFERENCED_AGG could use more documentation here.

Removed. They are obsolete now (and were already in v24---sorry!).

 > period?  Like we did for the primary keys (bool without_overlaps).

Okay, changed. Also in ATExecAddConstraint we can treat the PERIOD element like any other FK 
element, which simplifies the changes there a lot.

 > unreserved for this patch and upgrade it in the later one.

I tried doing this but got a shift/reduce conflict, so it's still reserved here.

Thanks,

From 97d1fe5cc6aa483469c4d442a155b7ddab97882f Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 20 Feb 2024 19:35:23 -0800
Subject: [PATCH v25 1/8] Rename conwithoutoverlaps to conperiod

---
 src/backend/catalog/heap.c          |  4 ++--
 src/backend/catalog/pg_constraint.c |  4 ++--
 src/backend/commands/trigger.c      |  2 +-
 src/backend/commands/typecmds.c     |  2 +-
 src/backend/utils/adt/ruleutils.c   |  2 +-
 src/backend/utils/cache/relcache.c  |  6 +++---
 src/bin/pg_dump/pg_dump.c           | 12 ++++++------
 src/bin/pg_dump/pg_dump.h           |  2 +-
 src/bin/psql/describe.c             |  4 ++--
 src/include/catalog/pg_constraint.h |  4 ++--
 10 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 348943e36cc..5e773740f4d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2145,7 +2145,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 							  is_local, /* conislocal */
 							  inhcount, /* coninhcount */
 							  is_no_inherit,	/* connoinherit */
-							  false,	/* conwithoutoverlaps */
+							  false,	/* conperiod */
 							  is_internal); /* internally constructed? */
 
 	pfree(ccbin);
@@ -2196,7 +2196,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  is_local,
 							  inhcount,
 							  is_no_inherit,
-							  false,	/* conwithoutoverlaps */
+							  false,	/* conperiod */
 							  false);
 	return constrOid;
 }
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 0a95608179d..3516fd58493 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -78,7 +78,7 @@ CreateConstraintEntry(const char *constraintName,
 					  bool conIsLocal,
 					  int conInhCount,
 					  bool conNoInherit,
-					  bool conWithoutOverlaps,
+					  bool conPeriod,
 					  bool is_internal)
 {
 	Relation	conDesc;
@@ -194,7 +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);
+	values[Anum_pg_constraint_conperiod - 1] = BoolGetDatum(conPeriod);
 
 	if (conkeyArray)
 		values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c344ff09442..2667dede218 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -841,7 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 											  true, /* islocal */
 											  0,	/* inhcount */
 											  true, /* noinherit */
-											  false,	/* conwithoutoverlaps */
+											  false,	/* conperiod */
 											  isInternal);	/* is_internal */
 	}
 
index e0275e5fe9c..08ed486ccbc 100644
-							  false,	/* conwithoutoverlaps */
+							  false,	/* conperiod */
index a928a8c55df..5727dfc4031 100644
 
 				keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
-				if (conForm->conwithoutoverlaps)
+				if (conForm->conperiod)
 					appendStringInfoString(&buf, " WITHOUT OVERLAPS");
 
 				appendStringInfoChar(&buf, ')');
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 50acae45298..d31b8cf08ce 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5622,9 +5622,9 @@ RelationGetExclusionInfo(Relation indexRelation,
 
 		/* We want the exclusion constraint owning the index */
 		if ((conform->contype != CONSTRAINT_EXCLUSION &&
-			 !(conform->conwithoutoverlaps && (
-											   conform->contype == CONSTRAINT_PRIMARY
-											   || conform->contype == CONSTRAINT_UNIQUE))) ||
+			 !(conform->conperiod && (
+									  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 2225a12718b..10cbf02bebd 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7252,7 +7252,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				i_conname,
 				i_condeferrable,
 				i_condeferred,
-				i_conwithoutoverlaps,
+				i_conperiod,
 				i_contableoid,
 				i_conoid,
 				i_condef,
@@ -7341,10 +7341,10 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 	if (fout->remoteVersion >= 170000)
 		appendPQExpBufferStr(query,
-							 "c.conwithoutoverlaps ");
+							 "c.conperiod ");
 	else
 		appendPQExpBufferStr(query,
-							 "NULL AS conwithoutoverlaps ");
+							 "NULL AS conperiod ");
 
 	/*
 	 * The point of the messy-looking outer join is to find a constraint that
@@ -7412,7 +7412,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_conperiod = PQfnumber(res, "conperiod");
 	i_contableoid = PQfnumber(res, "contableoid");
 	i_conoid = PQfnumber(res, "conoid");
 	i_condef = PQfnumber(res, "condef");
@@ -7520,7 +7520,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->conperiod = *(PQgetvalue(res, j, i_conperiod)) == 't';
 				constrinfo->conislocal = true;
 				constrinfo->separate = true;
 
@@ -17158,7 +17158,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 								  (k == 0) ? "" : ", ",
 								  fmtId(attname));
 			}
-			if (coninfo->conwithoutoverlaps)
+			if (coninfo->conperiod)
 				appendPQExpBufferStr(q, " WITHOUT OVERLAPS");
 
 			if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 77db42e354b..f49eb88dcbf 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -479,7 +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
+	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 */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b6a4eb1d565..c05befbb6f2 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2380,9 +2380,9 @@ describeOneTableDetails(const char *schemaname,
 				appendPQExpBufferStr(&buf, ", false AS indisreplident");
 			appendPQExpBufferStr(&buf, ", c2.reltablespace");
 			if (pset.sversion >= 170000)
-				appendPQExpBufferStr(&buf, ", con.conwithoutoverlaps");
+				appendPQExpBufferStr(&buf, ", con.conperiod");
 			else
-				appendPQExpBufferStr(&buf, ", false AS conwithoutoverlaps");
+				appendPQExpBufferStr(&buf, ", false AS conperiod");
 			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"
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 01e6bc21cd1..a33b4f17ea8 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -111,7 +111,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	 * For primary keys and unique constraints, signifies the last column uses
 	 * overlaps instead of equals.
 	 */
-	bool		conwithoutoverlaps;
+	bool		conperiod;

@@ -245,7 +245,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conIsLocal,
 								  int conInhCount,
 								  bool conNoInherit,
-								  bool conWithoutOverlaps,
+								  bool conPeriod,
 								  bool is_internal);

From a8edd93dd9573c2027bc7480566d277dda481347 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 29 Dec 2023 14:36:27 -0800
Subject: [PATCH v25 2/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                 | 114 ++++++++++++++++++++++++-
 doc/src/sgml/xindex.sgml               |   8 +-
 src/backend/access/gist/gistvalidate.c |   7 +-
 src/backend/access/index/amvalidate.c  |  24 +++++-
 src/include/access/amvalidate.h        |   1 +
 src/include/access/gist.h              |   3 +-
 src/include/catalog/pg_amproc.dat      |   6 ++
 7 files changed, 156 insertions(+), 7 deletions(-)

index 8a19f156d83..e67dd4b859f 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
@@ -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> value(s) into one
+   <literal>WITHOUT OVERLAPS</literal> span to compare against referencing rows.
  </para>
 
  <variablelist>
 
   <para>
 
   <para>
index 698e01ed2f7..ac3d9e50f50 100644
+											   1, 1, opcintype)
+					&& check_amproc_is_aggregate(procform->amproc);
index 32bb477f328..9eb1b172ae1 100644
@@ -140,13 +140,30 @@ identify_opfamily_groups(CatCList *oprlist, CatCList *proclist)
 	return result;
 }
 
+bool
+check_amproc_is_aggregate(Oid funcid)
+{
+	bool		result;
+	HeapTuple	tp;
+	Form_pg_proc procform;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+	procform = (Form_pg_proc) GETSTRUCT(tp);
+	result = procform->prokind == 'a';
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * Validate the signature (argument and result types) of an opclass support
  * function.  Return true if OK, false if not.
  *
  * 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 +180,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);

 	va_start(ap, maxargs);
diff --git a/src/include/access/amvalidate.h b/src/include/access/amvalidate.h
index 424ab63fa5a..c795a4bc1bf 100644
--- a/src/include/access/amvalidate.h
+++ b/src/include/access/amvalidate.h
@@ -28,6 +28,7 @@ typedef struct OpFamilyOpFuncGroup
 
 /* Functions in access/index/amvalidate.c */
 extern List *identify_opfamily_groups(CatCList *oprlist, CatCList *proclist);
+extern bool check_amproc_is_aggregate(Oid funcid);
 extern bool check_amproc_signature(Oid funcid, Oid restype, bool exact,
 								   int minargs, int maxargs,...);
 extern bool check_amoptsproc_signature(Oid funcid);
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
 
 /*
 
 # gin

From a941aad905d9528b745dab4b052f4d99514d708a Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 28 Feb 2024 21:45:37 -0800
Subject: [PATCH v25 3/8] Refactor FK operator lookup

Define FindFKComparisonOperators to do the work looking up the operators
for foreign keys. This cuts down on the length of
ATAddForeignKeyConstraint.
---
 src/backend/commands/tablecmds.c | 398 +++++++++++++++++--------------
 1 file changed, 217 insertions(+), 181 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f7987945563..02e5d8f8e38 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -551,6 +551,14 @@ 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,
+									  Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut);
 static void ATExecDropConstraint(Relation rel, const char *constrName,
 								 DropBehavior behavior, bool recurse,
 								 bool missing_ok, LOCKMODE lockmode);
@@ -9995,187 +10003,11 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
+		FindFKComparisonOperators(fkconstraint, tab, i, fkattnum,
+								  &pfeqoperators[i], &ppeqoperators[i],
+								  &ffeqoperators[i]);
 	}
 
 	/*
@@ -11227,6 +11059,210 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 	table_close(trigrel, RowExclusiveLock);
 }
 
+/*
+ * FindFKComparisonOperators -
+ *
+ * Gets the operators for pfeqopOut, ppeqopOut, and ffeqopOut.
+ * Sets old_check_ok if we can avoid re-validating the constraint.
+ * Sets old_pfeqop_item to the old pfeqop values.
+ */
+static void
+		char *fkattr_name = strVal(list_nth(fkconstraint->fk_attrs, i));
+		char *pkattr_name = strVal(list_nth(fkconstraint->pk_attrs, i));

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

- Added bison support for temporal FOREIGN KEYs. They should have a
  range/multirange/etc 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            |   46 +-
 src/backend/catalog/pg_constraint.c           |   56 +
 src/backend/commands/indexcmds.c              |   48 +-
 src/backend/commands/tablecmds.c              |  387 +++++--
 src/backend/parser/gram.y                     |   45 +-
 src/backend/utils/adt/ri_triggers.c           |  300 ++++-
 src/backend/utils/adt/ruleutils.c             |   19 +-
 src/include/catalog/pg_constraint.h           |   20 +-
 src/include/catalog/pg_proc.dat               |   24 +
 src/include/commands/defrem.h                 |    2 +-
 src/include/nodes/parsenodes.h                |    2 +
 src/include/parser/kwlist.h                   |    1 +
 src/test/regress/expected/btree_index.out     |   16 +-
 .../regress/expected/without_overlaps.out     | 1018 ++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     |  924 ++++++++++++++-
 17 files changed, 2815 insertions(+), 169 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 4cbaaccaf7c..0c39bad6c76 100644
+  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> ] ) ]
@@ -1152,8 +1152,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
-   <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,12 +1164,36 @@ 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.  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 treated normally
+      (and there must be at least one of them),
+      the <literal>PERIOD</literal> column is not compared for equality.
+      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.
+      Normally this column would be a range or multirange type,
+      although any type whose GiST opclass has a "contained by" operator
+      and a <literal>referenced_agg</literal> support function is allowed.
+      (See <xref linkend="gist-extensibility"/>.)
+      In addition the referenced table must have a primary key
+      or unique constraint declared with <literal>WITHOUT PORTION</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
@@ -1243,6 +1267,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>
 
@@ -1254,6 +1282,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>
 
@@ -1267,6 +1299,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 3516fd58493..bac1f8fc916 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
@@ -1607,6 +1608,61 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 	*numfks = numkeys;
 }
 
+/*
+ * FindFkPeriodOpersAndProcs -
+ *
+ * Looks up the oper and proc oids used by foreign keys with a PERIOD part.
+ */
+void
+FindFKPeriodOpersAndProcs(Oid opclass,
+						  Oid *periodoperoid,
+						  Oid *aggedperiodoperoid,
+						  Oid *periodprocoid)
+{
+	Oid	opfamily;
+	Oid	opcintype;
+	Oid	aggrettype;
+	Oid	funcid = InvalidOid;
+	StrategyNumber strat = RTContainedByStrategyNumber;
+
+	/*
+	 * Look up the ContainedBy operator with symmetric types.
+	 * 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.
+	 */
+	GetOperatorFromWellKnownStrategy(opclass,
+									 InvalidOid,
+									 periodoperoid,
+									 &strat);
+
+	/* Now 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.
+	 */
+	strat = RTContainedByStrategyNumber;
+	GetOperatorFromWellKnownStrategy(opclass,
+									 aggrettype,
+									 aggedperiodoperoid,
+									 &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 7a87626f5f0..5c7cc967e79 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -2181,8 +2181,10 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
 				strat = RTOverlapStrategyNumber;
 			else
 				strat = RTEqualStrategyNumber;
-			GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
-											 &opid, &strat);
+			GetOperatorFromWellKnownStrategy(opclassOids[attn],
+											 InvalidOid,
+											 &opid,
+											 &strat);
 			indexInfo->ii_ExclusionOps[attn] = opid;
 			indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
 			indexInfo->ii_ExclusionStrats[attn] = strat;
@@ -2421,7 +2423,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
  * opid - holds the operator we found
  * strat - holds the input and output strategy number
  *
@@ -2434,14 +2436,30 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
  * InvalidStrategy.
  */
 void
-GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
-								 Oid *opid, StrategyNumber *strat)
+GetOperatorFromWellKnownStrategy(Oid opclass,
+								 Oid rhstype,
+								 Oid *opid,
+								 StrategyNumber *strat)
 {
 	Oid			opfamily;
 	Oid			opcintype;
 	StrategyNumber instrat = *strat;
+	char	   *errstr;
 
-	Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
+	switch (instrat)
+	{
+		case RTEqualStrategyNumber:
+			errstr = "could not identify an equality operator for type %s";
+			break;
+		case RTOverlapStrategyNumber:
+			errstr = "could not identify an overlaps operator for type %s";
+			break;
+		case RTContainedByStrategyNumber:
+			errstr = "could not identify a contained by operator for type %s";
+			break;
+		default:
+			elog(ERROR, "unsupported strategy number %u", instrat);
+	}
 
 	*opid = InvalidOid;
 
@@ -2464,16 +2482,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(errstr, format_type_be(opcintype)),
 					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))
@@ -2486,9 +2508,7 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
 
 		ereport(ERROR,
+				errmsg(errstr, format_type_be(opcintype)),
index 02e5d8f8e38..457a40b66a0 100644
 
 #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,6 +213,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 */
@@ -387,16 +389,16 @@ 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_period);
 static Oid	transformFkeyCheckAttrs(Relation pkrel,
 									int numattrs, int16 *attnums,
-									Oid *opclasses);
+									bool is_temporal, 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);
@@ -509,7 +511,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);
@@ -519,7 +522,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);
@@ -558,6 +563,7 @@ static void FindFKComparisonOperators(Constraint *fkconstraint,
 									  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,
@@ -5973,7 +5979,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,

 				/*
 				 * No need to mark the constraint row as validated, we did
@@ -9823,6 +9830,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		is_temporal;
+	bool		pk_with_period;
 	int			i;
 	int			numfks,
 				numpks,
@@ -9917,6 +9926,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	numfks = transformColumnNameList(RelationGetRelid(rel),
 									 fkconstraint->fk_attrs,
 									 fkattnum, fktypoid);
+	is_temporal = fkconstraint->fk_with_period || fkconstraint->pk_with_period;
+	if (is_temporal)
+	{
+		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")));
+	}
 
 	numfkdelsetcols = transformColumnNameList(RelationGetRelid(rel),
 											  fkconstraint->fk_del_set_cols,
@@ -9936,16 +9953,32 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
 											&fkconstraint->pk_attrs,
 											pkattnum, pktypoid,
-											opclasses);
+											opclasses, &pk_with_period);
+
+		/* If the primary key uses WITHOUT OVERLAPS, the fk must use PERIOD */
+		if (pk_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")));
 	}
 	else
 	{
 		numpks = transformColumnNameList(RelationGetRelid(pkrel),
 										 fkconstraint->pk_attrs,
 										 pkattnum, pktypoid);
+
+		if (is_temporal)
+		{
+			if (!fkconstraint->pk_with_period)
+				/* Since we got pk_attrs, one should be a 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);
+										   is_temporal, opclasses);
 	}
 
 	/*
@@ -10006,10 +10039,28 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		FindFKComparisonOperators(fkconstraint, tab, i, fkattnum,
 								  &old_check_ok, &old_pfeqop_item,
 								  pktypoid[i], fktypoid[i], opclasses[i],
+								  is_temporal, is_temporal && i == numpks - 1,
 								  &pfeqoperators[i], &ppeqoperators[i],
 								  &ffeqoperators[i]);
 	}
 
+	/*
+	 * 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, even if we don't use the values.
+	 */
+	if (is_temporal)
+	{
+		Oid			periodoperoid;
+		Oid			aggedperiodoperoid;
+		Oid			periodprocoid;
+
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid, &periodprocoid);
+	}
+
 	/*
 	 * Create all the constraint and trigger objects, recursing to partitions
 	 * as necessary.  First handle the referenced side.
@@ -10026,7 +10077,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 									 numfkdelsetcols,
 									 fkdelsetcols,
 									 old_check_ok,
-									 InvalidOid, InvalidOid);
+									 InvalidOid, InvalidOid,
+									 is_temporal);
 
@@ -10042,7 +10094,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
 	/*
@@ -10127,7 +10180,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -10213,7 +10267,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
@@ -10289,7 +10343,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
 
 			/* Done -- clean up (but keep the lock) */
 			table_close(partRel, NoLock);
@@ -10347,7 +10402,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;
@@ -10395,6 +10451,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);
@@ -10512,7 +10569,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									  false,
 									  1,
 									  false,
-									  false,	/* conWithoutOverlaps */
+									  is_temporal,	/* conWithoutOverlaps */
 									  false);
 
 			/*
@@ -10543,7 +10600,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
 									old_check_ok,
 									lockmode,
 									insertTriggerOid,
-									updateTriggerOid);
+									updateTriggerOid,
+									is_temporal);
 
 			table_close(partition, NoLock);
 		}
@@ -10779,7 +10837,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
 							   confdelsetcols,
 							   true,
 							   deleteTriggerOid,
-							   updateTriggerOid);
+							   updateTriggerOid,
+							   constrForm->conperiod);
 
 		table_close(fkRel, NoLock);
 		ReleaseSysCache(tuple);
@@ -10872,6 +10931,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 		ListCell   *lc;
 		Oid			insertTriggerOid,
 					updateTriggerOid;
+		bool		is_temporal;
 
 		tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid));
 		if (!HeapTupleIsValid(tuple))
@@ -10987,6 +11047,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 			fkconstraint->conname = pstrdup(NameStr(constrForm->conname));
 
 		indexOid = constrForm->conindid;
+		is_temporal = constrForm->conperiod;
 		constrOid =
 			CreateConstraintEntry(fkconstraint->conname,
 								  constrForm->connamespace,
@@ -11018,7 +11079,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 */
@@ -11052,7 +11113,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
 								false,	/* no old check exists */
 								AccessExclusiveLock,
 								insertTriggerOid,
-								updateTriggerOid);
+								updateTriggerOid,
+								is_temporal);
 		table_close(pkrel, NoLock);
 	}
 
@@ -11074,6 +11136,7 @@ FindFKComparisonOperators(Constraint *fkconstraint,
 						  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;
@@ -11085,8 +11148,10 @@ FindFKComparisonOperators(Constraint *fkconstraint,
 	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));
@@ -11098,16 +11163,51 @@ FindFKComparisonOperators(Constraint *fkconstraint,
 	opcintype = cla_tup->opcintype;
 	ReleaseSysCache(cla_ht);
 
-	/*
+		 * GiST indexes are required to support temporal foreign keys
+		 * because they combine equals and overlaps.
+		 * An opclass can use whatever strategy numbers it wants, so we ask
+		 * the opclass what number it actually uses instead of our
+		 * RT* constants.
 
 	/*
 	 * There had better be a primary equality operator for the index.
@@ -11763,7 +11863,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_RI_FKEY_PERIOD_NOACTION_DEL &&
+				tgform->tgfoid != F_RI_FKEY_PERIOD_NOACTION_UPD &&
+				tgform->tgfoid != F_RI_FKEY_PERIOD_CHECK_INS &&
+				tgform->tgfoid != F_RI_FKEY_PERIOD_CHECK_UPD)
 				continue;
 
 			tgCopyTuple = heap_copytuple(tgtuple);
@@ -12077,7 +12181,7 @@ static int
 transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 						   List **attnamelist,
 						   int16 *attnums, Oid *atttypids,
-						   Oid *opclasses)
+						   Oid *opclasses, bool *pk_period)
 {
 	List	   *indexoidlist;
 	ListCell   *indexoidscan;
@@ -12141,7 +12245,8 @@ 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++)
@@ -12155,6 +12260,8 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 							   makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))));
 	}
 
+	*pk_period = (indexStruct->indisexclusion);
+
 	ReleaseSysCache(indexTuple);
 
 	return i;
@@ -12175,7 +12282,7 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
 static Oid
 transformFkeyCheckAttrs(Relation pkrel,
 						int numattrs, int16 *attnums,
-						Oid *opclasses)
+						bool is_temporal, Oid *opclasses)
 {
 	Oid			indexoid = InvalidOid;
 	bool		found = false;
@@ -12222,12 +12329,13 @@ transformFkeyCheckAttrs(Relation pkrel,
 		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
 
 		/*
-		 * Must have the right number of columns; must be unique and not a
+		 * 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 &&
+			(is_temporal ? indexStruct->indisexclusion : indexStruct->indisunique) &&
 			indexStruct->indisvalid &&
 			heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) &&
 			heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -12265,6 +12373,13 @@ transformFkeyCheckAttrs(Relation pkrel,
 				if (!found)
 					break;
 			}
+			/* The last attribute in the index must be the PERIOD FK part */
+			if (found && is_temporal)
+			{
+				int16 periodattnum = attnums[numattrs - 1];
+
+				found = (periodattnum == indexStruct->indkey.values[numattrs - 1]);
+			}
 
 			/*
 			 * Refuse to use a deferrable unique/primary key.  This is per SQL
@@ -12374,7 +12489,8 @@ validateForeignKeyConstraint(char *conname,
 							 Relation rel,
 							 Relation pkrel,
 							 Oid pkindOid,
-							 Oid constraintOid)
+							 Oid constraintOid,
+							 bool temporal)
 {
 	TupleTableSlot *slot;
 	TableScanDesc scan;
@@ -12403,8 +12519,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;
 
 	/*
@@ -12473,6 +12591,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 {
 	ObjectAddress trigAddress;
 	CreateTrigStmt *fk_trigger;
+	bool is_temporal = fkconstraint->fk_with_period;
 
 	/*
 	 * Note: for a self-referential FK (referencing and referenced tables are
@@ -12492,12 +12611,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("RI_FKey_period_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("RI_FKey_period_check_upd");
+		else
+			fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
 		fk_trigger->events = TRIGGER_TYPE_UPDATE;
 	}
 
@@ -12555,37 +12680,69 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
+	if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_noaction_del");
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_restrict_del");
+						 errmsg("invalid %s action for foreign key constraint using PERIOD",
 
@@ -12615,37 +12772,69 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
+	if (fkconstraint->fk_with_period)
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_noaction_upd");
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_restrict_upd");
+						 errmsg("invalid %s action for foreign key constraint using PERIOD",
 
index 130f7fc7c3f..a9366733f20 100644

 
@@ -4222,21 +4223,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);
@@ -4264,6 +4275,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 +17639,7 @@ reserved_keyword:
 			| ONLY
 			| OR
 			| ORDER
+			| PERIOD
 			| PLACING
 			| PRIMARY
 			| REFERENCES
@@ -17927,6 +17949,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..f5c65712b4b 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -31,6 +31,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
@@ -48,6 +49,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 +120,17 @@ 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 PERIOD SQL */
+	Oid			agged_period_contained_by_oper;	/* operator for PERIOD SQL */
+	Oid			period_referenced_agg_proc;	/* proc for PERIOD SQL */
+	Oid			period_referenced_agg_rettype;	/* rettype for previous */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -231,6 +238,7 @@ 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 lookupPeriodRIProc(const RI_ConstraintInfo *riinfo, char **aggname);

 /*
@@ -365,14 +373,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->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++)
 		{
@@ -390,6 +425,21 @@ RI_FKey_check(TriggerData *trigdata)
 			queryoids[i] = fk_type;
 		}
 		appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+		if (riinfo->temporal)
+		{
+			char   *aggname;
+			Oid		fk_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+			Oid		agg_rettype = riinfo->period_referenced_agg_rettype;
+
+			lookupPeriodRIProc(riinfo, &aggname);
+			appendStringInfo(&querybuf, ") x1 HAVING ");
+			sprintf(paramname, "$%d", riinfo->nkeys);
+			ri_GenerateQual(&querybuf, "",
+							paramname, fk_type,
+							riinfo->agged_period_contained_by_oper,
+							aggname, agg_rettype);
+			appendStringInfo(&querybuf, "(x1.r)");
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -497,14 +547,40 @@ 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++)
 		{
@@ -521,6 +597,21 @@ 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   *aggname;
+			Oid		fk_type = RIAttType(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1]);
+			Oid		agg_rettype = riinfo->period_referenced_agg_rettype;
+
+			lookupPeriodRIProc(riinfo, &aggname);
+			appendStringInfo(&querybuf, ") x1 HAVING ");
+			sprintf(paramname, "$%d", riinfo->nkeys);
+			ri_GenerateQual(&querybuf, "",
+							paramname, fk_type,
+							riinfo->agged_period_contained_by_oper,
+							aggname, agg_rettype);
+			appendStringInfo(&querybuf, "(x1.r)");
+		}
 
 		/* Prepare and save the plan */
 		qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -1213,6 +1304,126 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind)
 	}
 }
 
+/* ----------
+ * RI_FKey_period_check_ins -
+ *
+ *	Check temporal foreign key existence at insert event on FK table.
+ * ----------
+ */
+Datum
+RI_FKey_period_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);
+}
+
+
+/* ----------
+ * RI_FKey_period_check_upd -
+ *
+ *	Check temporal foreign key existence at update event on FK table.
+ * ----------
+ */
+Datum
+RI_FKey_period_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);
+}
+
+
+/* ----------
+ * RI_FKey_period_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
+RI_FKey_period_noaction_del(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Check that this is a valid trigger call on the right time and event.
+	 */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_noaction_del", RI_TRIGTYPE_DELETE);
+
+	/*
+	 * Share code with RESTRICT/UPDATE cases.
+	 */
+	return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * RI_FKey_period_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
+RI_FKey_period_restrict_del(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_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
+RI_FKey_period_noaction_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_noaction_upd", RI_TRIGTYPE_UPDATE);
+
+	/* Share code with RESTRICT/DELETE cases. */
+	return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * RI_FKey_period_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
+RI_FKey_period_restrict_upd(PG_FUNCTION_ARGS)
+{
+	/* Check that this is a valid trigger call on the right time and event. */
+	ri_CheckTrigger(fcinfo, "RI_FKey_period_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 -
@@ -2171,6 +2382,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
 	riinfo->confupdtype = conForm->confupdtype;
 	riinfo->confdeltype = conForm->confdeltype;
 	riinfo->confmatchtype = conForm->confmatchtype;
+	riinfo->temporal = conForm->conperiod;
 
 	DeconstructFkConstraintRow(tup,
 							   &riinfo->nkeys,
@@ -2182,6 +2394,22 @@ 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->temporal)
+	{
+		Oid	opclass = get_index_column_opclass(conForm->conindid, riinfo->nkeys);
+		FindFKPeriodOpersAndProcs(opclass,
+								  &riinfo->period_contained_by_oper,
+								  &riinfo->agged_period_contained_by_oper,
+								  &riinfo->period_referenced_agg_proc);
+		riinfo->period_referenced_agg_rettype = get_func_rettype(riinfo->period_referenced_agg_proc);
+	}
+
 	ReleaseSysCache(tup);
 
 	/*
@@ -2793,7 +3021,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
@@ -2849,13 +3080,23 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 		}
 		else
 		{
+			Oid eq_opr = riinfo->ff_eq_oprs[i];
+
+			/*
+			 * 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->temporal && i == riinfo->nkeys - 1)
+				eq_opr = riinfo->period_contained_by_oper;
+
 			/*
 			 * 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_AttributesEqual(eq_opr, RIAttType(rel, attnums[i]),
+									newvalue, oldvalue))
 				return false;
 		}
 	}
@@ -2863,7 +3104,6 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
 	return true;
 }
 
-
 /*
  * ri_AttributesEqual -
  *
@@ -3021,12 +3261,56 @@ 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_RESTRICT_DEL:
+		case F_RI_FKEY_PERIOD_RESTRICT_UPD:
+		case F_RI_FKEY_PERIOD_NOACTION_DEL:
+		case F_RI_FKEY_PERIOD_NOACTION_UPD:
 			return RI_TRIGGER_PK;
 
 		case F_RI_FKEY_CHECK_INS:
 		case F_RI_FKEY_CHECK_UPD:
+		case F_RI_FKEY_PERIOD_CHECK_INS:
+		case F_RI_FKEY_PERIOD_CHECK_UPD:
 			return RI_TRIGGER_FK;
 	}
 
 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * lookupPeriodRIProc -
+ *
+ * Gets the name of the aggregate function
+ * used to build the SQL for temporal RI constraints.
+ * Raises an error if not found.
+ */
+static void
+lookupPeriodRIProc(const RI_ConstraintInfo *riinfo, char **aggname)
+{
+	Oid			oid = riinfo->period_referenced_agg_proc;
+	HeapTuple	tp;
+	Form_pg_proc functup;
+	char	   *namesp;
+	char	   *func;
+
+
+	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.")));
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(riinfo->period_referenced_agg_proc));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", oid);
+
+	functup = (Form_pg_proc) GETSTRUCT(tp);
+	namesp = get_namespace_name(functup->pronamespace);
+	func = NameStr(functup->proname);
+
+	*aggname = psprintf("%s.%s", quote_identifier(namesp), quote_identifier(func));
+
+	pfree(namesp);
+	ReleaseSysCache(tp);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5727dfc4031..f7a44c7b330 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,
@@ -2246,7 +2246,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->conperiod, &buf);
 
@@ -2257,7 +2259,8 @@ 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);

@@ -2343,7 +2346,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				if (!isnull)
 				{
 
@@ -2378,7 +2381,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				val = SysCacheGetAttrNotNull(CONSTROID, tup,
 											 Anum_pg_constraint_conkey);
 
 				if (conForm->conperiod)
 
@@ -2576,7 +2579,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 +2598,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 a33b4f17ea8..e4f4c2ffaf1 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,20 @@ 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);
 
@@ -283,6 +283,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 FindFKPeriodOpersAndProcs(Oid opclass,
+									  Oid *periodoperoid,
+									  Oid *aggedperiodoperoid,
+									  Oid *periodprocoid);
 
 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 9c120fc2b7f..a8367ff5843 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 => 'RI_FKey_period_check_ins', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_check_ins' },
+{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES',
+  proname => 'RI_FKey_period_check_upd', provolatile => 'v', prorettype => 'trigger',
+  proargtypes => '', prosrc => 'RI_FKey_period_check_upd' },
+{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
+  proname => 'RI_FKey_period_restrict_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_restrict_del' },
+{ oid => '6127', descr => 'temporal referential integrity ON UPDATE RESTRICT',
+  proname => 'RI_FKey_period_restrict_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_restrict_upd' },
+{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
+  proname => 'RI_FKey_period_noaction_del', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_noaction_del' },
+{ oid => '6133', descr => 'temporal referential integrity ON UPDATE NO ACTION',
+  proname => 'RI_FKey_period_noaction_upd', provolatile => 'v',
+  prorettype => 'trigger', proargtypes => '',
+  prosrc => 'RI_FKey_period_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 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 baa6a97c7e2..ceca4c801bf 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2601,6 +2601,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 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/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 8311a03c3df..f3595d4ea3b 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -206,14 +206,16 @@ 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_noaction_del
+ RI_FKey_period_restrict_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(5 rows)
+(7 rows)
 
 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
@@ -252,14 +254,16 @@ 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_noaction_del
+ RI_FKey_period_restrict_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(5 rows)
+(7 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 726e94102bf..2139a393dde 100644
--- a/src/test/regress/expected/without_overlaps.out
+++ b/src/test/regress/expected/without_overlaps.out
@@ -98,21 +98,51 @@ CREATE TABLE temporal_rng3 (
 ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
 DROP TABLE temporal_rng3;
 DROP TYPE textrange2;
--- PK with a multirange:
+-- PK with one column plus a multirange:
 CREATE TABLE temporal_mltrng (
   id int4range,
-  valid_at tsmultirange,
+  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 | tsmultirange |           | not null | 
+               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)
 
+-- PK with two columns plus a multirange:
+-- We don't drop this table because tests below also need multiple scalar columns.
+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 tsrange,
@@ -289,6 +319,30 @@ 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).
+-- okay:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-01', NULL)));
+-- should fail:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', 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), {[01-01-2018,01-05-2018)}) conflicts with existing key (id, valid_at)=([1,2), {[01-02-2018,02-03-2018)}).
+INSERT INTO temporal_mltrng 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, {[01-01-2018,01-05-2018)}).
+INSERT INTO temporal_mltrng VALUES ('[3,3]', 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) | {[01-02-2018,02-03-2018)}
+ [1,2) | {[03-03-2018,04-04-2018)}
+ [2,3) | {[01-01-2018,01-05-2018)}
+ [3,4) | {[01-01-2018,)}
+(4 rows)
+
 --
 -- test a range with both a PK and a UNIQUE constraint
 --
@@ -393,3 +447,953 @@ 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
+--
+-- test table setup
+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);
+-- 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.
+-- works: PERIOD for both referenced and referencing
+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)
+-- REFERENCES part should specify PERIOD
+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)
+-- FOREIGN KEY part should specify PERIOD
+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]
+-- FOREIGN KEY part should specify PERIOD
+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
+DROP TABLE temporal_rng2;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at tsrange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+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;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng VALUES
+  ('[1,1]', tsrange('2018-01-02', '2018-02-03')),
+  ('[1,1]', tsrange('2018-03-03', '2018-04-04')),
+  ('[2,2]', tsrange('2018-01-01', '2018-01-05')),
+  ('[3,3]', tsrange('2018-01-01', NULL));
+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')),
+  ('[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 UPDATE 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')),
+  ('[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')),
+  ('[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')),
+  ('[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:  invalid ON DELETE action for foreign key constraint using PERIOD
+-- 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:  invalid ON DELETE action for foreign key constraint using PERIOD
+-- 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:  invalid ON DELETE action for foreign key constraint using PERIOD
+--
+-- test FOREIGN KEY, multirange references multirange
+--
+-- 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)
+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, 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, 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]
+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:
+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;
+-- 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
+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)
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+	ALTER COLUMN valid_at TYPE tsmultirange USING '{}'::tsmultirange;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+ERROR:  foreign key constraint "temporal_fk_mltrng2mltrng_fk" cannot be implemented
+DETAIL:  Key columns "valid_at" and "valid_at" are of incompatible types: tsmultirange and datemultirange.
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ALTER COLUMN valid_at TYPE datemultirange USING '{}'::datemultirange;
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+-- 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;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,1]');
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+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), {[01-02-2018,04-01-2018)}) 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;
+--
+-- 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 child inserts
+--
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+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), {[01-02-2018,04-01-2018)}) is not present in table "temporal_mltrng".
+-- now it should work:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-02-03', '2018-03-03')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+--
+-- test FK child updates
+--
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-03-01')) WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-05-01')) WHERE id = '[1,1]';
+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), {[01-02-2018,05-01-2018)}) is not present in table "temporal_mltrng".
+UPDATE temporal_fk_mltrng2mltrng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+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), {[01-02-2018,03-01-2018)}) is not present in table "temporal_mltrng".
+-- ALTER FK DEFERRABLE
+BEGIN;
+  INSERT INTO temporal_mltrng VALUES
+    ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+    ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+  INSERT INTO temporal_fk_mltrng2mltrng VALUES
+    ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+  DELETE FROM temporal_mltrng WHERE id = '[5,5]'; --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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+--
+-- test FK parent updates NO ACTION
+--
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng VALUES ('[5,5]', 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,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,5]' 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,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- changing the scalar part fails:
+UPDATE temporal_mltrng SET id = '[7,7]'
+WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
+--
+-- test FK parent updates RESTRICT
+--
+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
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng VALUES ('[5,5]', 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,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,5]' 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,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- changing the scalar part fails:
+UPDATE temporal_mltrng SET id = '[7,7]'
+WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
+--
+-- test FK parent deletes NO ACTION
+--
+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;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng VALUES ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
+--
+-- test FK parent deletes RESTRICT
+--
+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
+	ON DELETE RESTRICT;
+INSERT INTO temporal_mltrng VALUES ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
+-- 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_valid_at_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_valid_at_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_valid_at_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_valid_at_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:  invalid ON DELETE action for foreign key constraint using PERIOD
+-- 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:  invalid ON DELETE action for foreign key constraint using PERIOD
+-- 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:  invalid ON DELETE action for foreign key constraint using PERIOD
+-- 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..540fc74eccd 100644
--- a/src/test/regress/sql/without_overlaps.sql
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -56,7 +56,6 @@ 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 a custom range type:
 CREATE TYPE textrange2 AS range (subtype=text, collation="C");
 CREATE TABLE temporal_rng3 (
@@ -68,14 +67,26 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk;
 DROP TABLE temporal_rng3;
 DROP TYPE textrange2;
 
--- PK with a multirange:
+-- PK with one column plus a multirange:
 CREATE TABLE temporal_mltrng (
   id int4range,
-  valid_at tsmultirange,
+  valid_at datemultirange,
   CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
 );
 \d temporal_mltrng
 
+-- PK with two columns plus a multirange:
+-- We don't drop this table because tests below also need multiple scalar columns.
+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 (
@@ -214,6 +225,19 @@ 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);
 
+-- okay:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-03')));
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-03-03', '2018-04-04')));
+INSERT INTO temporal_mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-01', NULL)));
+
+-- should fail:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng VALUES (NULL, datemultirange(daterange('2018-01-01', '2018-01-05')));
+INSERT INTO temporal_mltrng VALUES ('[3,3]', NULL);
+
+SELECT * FROM temporal_mltrng ORDER BY id, valid_at;
+
 --
 -- test a range with both a PK and a UNIQUE constraint
 --
@@ -288,3 +312,897 @@ 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
+--
+
+-- test table setup
+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);
+
+-- 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 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)
+-- REFERENCES part should specify PERIOD
+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)
+-- FOREIGN KEY part should specify PERIOD
+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]
+-- FOREIGN KEY part should specify PERIOD
+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
+DROP TABLE temporal_rng2;
+CREATE TABLE temporal_rng2 (
+  id1 int4range,
+  id2 int4range,
+  valid_at tsrange,
+  CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+
+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;
+DELETE FROM temporal_rng;
+INSERT INTO temporal_rng VALUES
+  ('[1,1]', tsrange('2018-01-02', '2018-02-03')),
+  ('[1,1]', tsrange('2018-03-03', '2018-04-04')),
+  ('[2,2]', tsrange('2018-01-01', '2018-01-05')),
+  ('[3,3]', tsrange('2018-01-01', NULL));
+
+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')),
+  ('[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 UPDATE 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')),
+  ('[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')),
+  ('[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')),
+  ('[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;
+
+--
+-- test FOREIGN KEY, multirange references multirange
+--
+
+-- 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)
+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, 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, valid_at)
+		REFERENCES temporal_mltrng (id, PERIOD valid_at)
+);
+-- (parent_id, 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, 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:
+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;
+
+-- 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
+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
+
+-- with inferred PK on the referenced table, and wrong column type:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk,
+	ALTER COLUMN valid_at TYPE tsmultirange USING '{}'::tsmultirange;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ALTER COLUMN valid_at TYPE datemultirange USING '{}'::datemultirange;
+
+-- with inferred PK on the referenced table:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+
+-- 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;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,1]');
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES temporal_mltrng;
+ALTER TABLE temporal_fk_mltrng2mltrng
+	DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk;
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+-- should fail:
+ALTER TABLE temporal_fk_mltrng2mltrng
+	ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk
+	FOREIGN KEY (parent_id, PERIOD valid_at)
+	REFERENCES 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;
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_mltrng2mltrng_fk';
+
+--
+-- test FK child inserts
+--
+
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-01')), '[1,1]');
+-- should fail:
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+-- now it should work:
+INSERT INTO temporal_mltrng VALUES ('[1,1]', datemultirange(daterange('2018-02-03', '2018-03-03')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[2,2]', datemultirange(daterange('2018-01-02', '2018-04-01')), '[1,1]');
+
+--
+-- test FK child updates
+--
+
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-03-01')) WHERE id = '[1,1]';
+-- should fail:
+UPDATE temporal_fk_mltrng2mltrng SET valid_at = datemultirange(daterange('2018-01-02', '2018-05-01')) WHERE id = '[1,1]';
+UPDATE temporal_fk_mltrng2mltrng SET parent_id = '[8,8]' WHERE id = '[1,1]';
+
+-- ALTER FK DEFERRABLE
+
+BEGIN;
+  INSERT INTO temporal_mltrng VALUES
+    ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+    ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+  INSERT INTO temporal_fk_mltrng2mltrng VALUES
+    ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+  ALTER TABLE temporal_fk_mltrng2mltrng
+    ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk
+    DEFERRABLE INITIALLY DEFERRED;
+
+  DELETE FROM temporal_mltrng 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_mltrng VALUES ('[5,5]', 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,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- changing the scalar part fails:
+UPDATE temporal_mltrng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
+
+--
+-- test FK parent updates RESTRICT
+--
+
+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
+	ON UPDATE RESTRICT;
+-- a PK update that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng VALUES ('[5,5]', 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,5]';
+-- a PK update that succeeds even though the numeric id is referenced because the range isn't:
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01'))
+WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- changing the scalar part fails:
+UPDATE temporal_mltrng SET id = '[7,7]'
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
+--
+-- test FK parent deletes NO ACTION
+--
+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;
+-- a PK delete that succeeds because the numeric id isn't referenced:
+INSERT INTO temporal_mltrng VALUES ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
+
+--
+-- test FK parent deletes RESTRICT
+--
+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
+	ON DELETE RESTRICT;
+INSERT INTO temporal_mltrng VALUES ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01')));
+DELETE FROM temporal_mltrng WHERE id = '[5,5]';
+-- a PK delete that succeeds even though the numeric id is referenced because the range isn't:
+INSERT INTO temporal_mltrng VALUES
+  ('[5,5]', datemultirange(daterange('2018-01-01', '2018-02-01'))),
+  ('[5,5]', datemultirange(daterange('2018-02-01', '2018-03-01')));
+INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,5]');
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- clean up:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
+DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
+
+-- 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 278280f41aab510857d3378790c4dfda5d5117be Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 30 Dec 2023 23:10:59 -0800
Subject: [PATCH v25 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 an array of 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        |  31 ++--
 src/backend/access/index/amvalidate.c         |   6 +-
 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, 676 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 880f717b103..6f6c8a52049 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 e67dd4b859f..bbf7841807d 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 eight that are optional.
+   <acronym>GiST</acronym> must provide, and ten 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
@@ -304,6 +304,12 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    temporal foreign keys to combined referenced rows with the same
    non-<literal>WITHOUT OVERLAPS</literal> value(s) into one
    <literal>WITHOUT OVERLAPS</literal> span to compare against referencing rows.
+   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.
+   The optional fifteenth method <function>without_portion</function> is used by
+   <literal>FOR PORTION OF</literal> to compute the leftover records outside the
+   targeted bounds.
  </para>
 
  <variablelist>
@@ -1352,6 +1358,153 @@ my_range_agg_finalfn(PG_FUNCTION_ARGS)
 
     PG_RETURN_MULTIRANGE_P(make_multirange(mltrngtypoid, typcache-&gt;rngtype, range_count, ranges));
 }
+</programlisting>
+      </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
+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>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 93df136eba3..d4d0f6dc685 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
-   GiST indexes have thirteen support functions, eight of which are optional,
+   GiST indexes have fifteen support functions, ten of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -602,6 +602,18 @@
         part</entry>
        <entry>13</entry>
       </row>
+      <row>
+       <entry><function>intersect</function></entry>
+       <entry>computes intersection with <literal>FOR PORTION OF</literal>
+        bounds</entry>
+       <entry>14</entry>
+      </row>
+      <row>
+       <entry><function>without_portion</function></entry>
+       <entry>computes remaining duration(s) outside
+       <literal>FOR PORTION OF</literal> bounds</entry>
+       <entry>15</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 13cf390bb3d..5a11e0cea25 100644
--- a/src/backend/access/gin/ginvalidate.c
+++ b/src/backend/access/gin/ginvalidate.c
@@ -106,37 +106,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 ac3d9e50f50..0a4338e8b84 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -107,36 +107,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;
@@ -144,18 +144,26 @@ 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_REFERENCED_AGG_PROC:
-				ok = check_amproc_signature(procform->amproc, InvalidOid, false,
+				ok = check_amproc_signature(procform->amproc, InvalidOid, false, false,
 											   1, 1, opcintype)
 					&& check_amproc_is_aggregate(procform->amproc);
 				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),
@@ -277,7 +285,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_REFERENCED_AGG_PROC)
+			i == GIST_STRATNUM_PROC || i == GIST_REFERENCED_AGG_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 9eb1b172ae1..cc27cc28ab5 100644
--- a/src/backend/access/index/amvalidate.c
+++ b/src/backend/access/index/amvalidate.c
@@ -166,7 +166,7 @@ check_amproc_is_aggregate(Oid funcid)
  * unless it is InvalidOid.
  */
 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;
@@ -181,7 +181,7 @@ check_amproc_signature(Oid funcid, Oid restype, bool exact,
 	procform = (Form_pg_proc) GETSTRUCT(tp);
 
 	if ((procform->prorettype != restype && OidIsValid(restype))
-		|| procform->proretset || procform->pronargs < minargs
+		|| procform->proretset != retset || procform->pronargs < minargs
 		|| procform->pronargs > maxargs)
 		result = false;
 
@@ -209,7 +209,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 8834f22ba85..870cb395761 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 f82e6f42d98..5a6a5041c42 100644
--- a/src/backend/utils/adt/multirangetypes.c
+++ b/src/backend/utils/adt/multirangetypes.c
@@ -1226,6 +1226,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 c795a4bc1bf..4844ba82dc3 100644
--- a/src/include/access/amvalidate.h
+++ b/src/include/access/amvalidate.h
@@ -29,8 +29,8 @@ typedef struct OpFamilyOpFuncGroup
 /* Functions in access/index/amvalidate.c */
 extern List *identify_opfamily_groups(CatCList *oprlist, CatCList *proclist);
 extern bool check_amproc_is_aggregate(Oid funcid);
-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 641677e191c..e8b393f9dfd 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -41,7 +41,9 @@
 #define GIST_SORTSUPPORT_PROC			11
 #define GIST_STRATNUM_PROC				12
 #define GIST_REFERENCED_AGG_PROC		13
-#define GISTNProcs					13
+#define GIST_INTERSECT_PROC				14
+#define GIST_WITHOUT_PORTION_PROC		15
+#define GISTNProcs					15
 
 /*
  * 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 1d3d5fcf4d8..8c922974bb1 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 => '14',
+  amproc => 'box_intersect(box,box)' },
 { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon',
   amprocrighttype => 'polygon', amprocnum => '1',
   amproc => 'gist_poly_consistent' },
@@ -613,6 +616,12 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '13',
   amproc => 'range_agg(anyrange)' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '14',
+  amproc => 'range_intersect(anyrange,anyrange)' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '15',
+  amproc => 'range_without_portion(anyrange,anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -656,6 +665,12 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '13',
   amproc => 'range_agg(anymultirange)' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '14',
+  amproc => 'multirange_intersect(anymultirange,anymultirange)' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '15',
+  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 a8367ff5843..7fe2542f058 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10582,6 +10582,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' },
@@ -10869,6 +10873,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 1dae72522d6f22adfbb41c1a4f92940e0012a171 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Fri, 25 Jun 2021 18:54:35 -0700
Subject: [PATCH v25 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/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       |   8 +-
 src/backend/optimizer/plan/planner.c          |   1 +
 src/backend/optimizer/util/pathnode.c         |   4 +-
 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              |   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                 |   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     | 144 +++-
 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     |  93 ++-
 50 files changed, 2734 insertions(+), 68 deletions(-)
 create mode 100644 src/test/regress/expected/for_portion_of.out
 create mode 100644 src/test/regress/sql/for_portion_of.sql

index c355e8f3f7d..97b49f0c1f1 100644
 
index 812e7646e16..603cb73e978 100644
 
+-- 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/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 6f6c8a52049..6ce6d0a5011 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 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>
 
+   <varlistentry>
 
+  <para>
 
+   <varlistentry>

diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 0a4338e8b84..7f70c322f43 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -207,7 +207,7 @@ gistvalidate(Oid opclassoid)
 		}
 
@@ -234,24 +234,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 83d00a46638..0d8d17de444 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3984,7 +3984,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 457a40b66a0..04bd325e7ac 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12562,6 +12562,7 @@ validateForeignKeyConstraint(char *conname,
 		trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		trigdata.tg_trigslot = slot;
 		trigdata.tg_trigger = &trig;
+		trigdata.tg_temporal = NULL;

index 2667dede218..39baf31f25c 100644

 

 

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

 /*
@@ -1205,6 +1212,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
  *
@@ -1357,7 +1527,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 +1561,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		ar_delete_trig_tcs = NULL;
 	}
 
@@ -1770,7 +1946,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2139,6 +2319,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4303,6 +4488,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
index 610f4a56d6b..59df44702d1 100644

@@ -7014,7 +7016,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
@@ -7080,6 +7083,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
index be4e182869a..4c438706996 100644
@@ -1966,6 +1966,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
 
index 6f79b2e3fe7..4900b4f6d70 100644
@@ -3717,7 +3717,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
@@ -3783,6 +3784,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index dbdf6bf8964..418919ced90 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -24,7 +24,10 @@
 
 #include "postgres.h"
 
@@ -47,11 +50,13 @@
 
@@ -60,10 +65,16 @@
 
@@ -479,6 +490,20 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
 	return result;
 }
 
+static Node *
@@ -512,6 +537,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 
 	qry->commandType = CMD_DELETE;
@@ -550,7 +576,11 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -1173,7 +1203,7 @@ transformOnConflictClause(ParseState *pstate,
 
 		onConflictWhere = transformWhereClause(pstate,
 											   onConflictClause->whereClause,
@@ -1203,6 +1233,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
@@ -2409,6 +2620,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 {
 	Query	   *qry = makeNode(Query);
 	ParseNamespaceItem *nsitem;
+	Node	   *whereClause;
 	Node	   *qual;
 
@@ -2426,6 +2638,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
 	/* subqueries in FROM cannot access the result relation */
@@ -2442,7 +2658,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 	nsitem->p_lateral_only = false;
 	nsitem->p_lateral_ok = true;

@@ -2451,7 +2668,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
 
@@ -2470,7 +2687,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
@@ -2523,6 +2740,21 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
 
+		/*
index a9366733f20..ca75eec7b75 100644

 
 	QUOTE
@@ -12239,14 +12252,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;
 				}
@@ -12309,6 +12324,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
@@ -12317,10 +12333,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;
 				}
@@ -13756,6 +13773,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
  */
@@ -17349,6 +17387,7 @@ unreserved_keyword:
 			| PASSWORD
 			| PLANS
 			| POLICY
+			| PORTION
 			| PRECEDING
 			| PREPARE
 			| PREPARED
@@ -17953,6 +17992,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 9d151a880b8..2591ffa34f7 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -575,6 +575,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;
 
 			/*
@@ -965,6 +972,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 
 			/*
 
 			/*
 
 			/*
 
 			/*
index 73f7a48b3c6..741e7c3f1a9 100644
@@ -377,7 +377,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 
index f5c65712b4b..6a9d35f35fd 100644
@@ -239,6 +240,11 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 static void lookupPeriodRIProc(const RI_ConstraintInfo *riinfo, char **aggname);

 /*
 
@@ -624,6 +631,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);
 
@@ -723,6 +731,8 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 	TupleTableSlot *oldslot;
 	RI_QueryKey qkey;
 	SPIPlanPtr	qplan;
+	int			targetRangeParam = -1;
+	Datum		targetRange = (Datum) 0;
 
@@ -813,9 +823,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
 
@@ -922,6 +939,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);
 
@@ -1043,6 +1061,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);
 
@@ -1275,6 +1294,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);
 
@@ -2550,6 +2570,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
@@ -2605,6 +2626,12 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/*
@@ -3314,3 +3341,50 @@ lookupPeriodRIProc(const RI_ConstraintInfo *riinfo, char **aggname)
 	pfree(namesp);
index f730aa26c47..53e706c6c01 100644
@@ -1606,7 +1606,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;
 
@@ -1620,6 +1620,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;
 }
@@ -2143,6 +2145,59 @@ get_typisdefined(Oid typid)
 		return false;
 }
 
+/*
+ * get_typname
+ *
index 8a47d3c9ec8..b801ef1e3c0 100644
@@ -79,7 +79,7 @@ typedef uint16 StrategyNumber;

 #endif							/* STRATNUM_H */
 
 /*

+/*
+ * ForPortionOfState
+ *

index ceca4c801bf..a20b39a4a28 100644

+/*
+ * ForPortionOfClause

 
+/*----------
 
index e4a200b00ec..48df107c1ce 100644
@@ -119,7 +119,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);
@@ -142,6 +142,8 @@ extern char get_rel_persistence(Oid relid);
index 00000000000..9cd80b4dfdc
@@ -0,0 +1,685 @@
+-- Test with multiranges
+  valid_at datemultirange NOT NULL,
+('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'),
+('[1,1]', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'),
+('[2,2]', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'),
+('[3,3]', 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')))
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))
+-------+---------------------------------------------------------------------------+-------
+ [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
index 7610b011d68..e811153a409 100644
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
index 2139a393dde..0c8224a35f5 100644
@@ -359,6 +359,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
@@ -411,6 +441,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -446,6 +506,36 @@ SELECT * FROM tp2 ORDER BY id, valid_at;
 
@@ -770,13 +860,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -808,13 +910,25 @@ UPDATE temporal_rng SET id = '[7,7]'
@@ -837,9 +951,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
@@ -862,9 +989,22 @@ DELETE FROM temporal_rng WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01',
 
 # ----------
index 00000000000..03d5cc75599
@@ -0,0 +1,531 @@
+-- Test with multiranges
+  valid_at datemultirange NOT NULL,
+('[1,1]', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'),
+('[1,1]', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'),
+('[2,2]', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'),
+('[3,3]', 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')))
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15')))

 
index 540fc74eccd..c111f6a6bca 100644
@@ -255,6 +255,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name)
 
 --
@@ -293,6 +307,22 @@ INSERT INTO temporal_partitioned VALUES
 
 -- temporal UNIQUE:
@@ -311,6 +341,22 @@ INSERT INTO temporal_partitioned VALUES
 
 --
@@ -608,13 +654,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
 
@@ -644,13 +700,23 @@ WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01');
@@ -671,14 +737,24 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-
 
@@ -696,9 +772,20 @@ INSERT INTO temporal_fk_rng2rng VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-

From 04e5cc31736448e7b7a4c7977c30f4bbcb05bc75 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Sat, 3 Jun 2023 21:41:11 -0400
Subject: [PATCH v25 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           |  24 +-
 src/backend/commands/tablecmds.c              |  53 +-
 src/backend/utils/adt/ri_triggers.c           | 575 ++++++++++-
 src/include/catalog/pg_constraint.h           |   7 +-
 src/include/catalog/pg_proc.dat               |  20 +
 src/test/regress/expected/btree_index.out     |  18 +-
 .../regress/expected/without_overlaps.out     | 941 +++++++++++++++++-
 src/test/regress/sql/without_overlaps.sql     | 496 ++++++++-
 9 files changed, 2073 insertions(+), 75 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 0c39bad6c76..3a791c89c33 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1269,7 +1269,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>
@@ -1284,7 +1286,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>
@@ -1301,7 +1306,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 bac1f8fc916..efba05920b6 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -1615,15 +1615,16 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
  */
 void
 FindFKPeriodOpersAndProcs(Oid opclass,
-						  Oid *periodoperoid,
-						  Oid *aggedperiodoperoid,
-						  Oid *periodprocoid)
+						  Oid *containedbyoperoid,
+						  Oid *aggedcontainedbyoperoid,
+						  Oid *aggprocoid,
+						  Oid *intersectprocoid)
 {
 	Oid	opfamily;
 	Oid	opcintype;
 	Oid	aggrettype;
 	Oid	funcid = InvalidOid;
-	StrategyNumber strat = RTContainedByStrategyNumber;
+	StrategyNumber strat;
 
 	/*
 	 * Look up the ContainedBy operator with symmetric types.
@@ -1631,9 +1632,10 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 	 * 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,
-									 periodoperoid,
+									 containedbyoperoid,
 									 &strat);
 
 	/* Now look up the support proc for aggregation. */
@@ -1646,7 +1648,7 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 				 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;
+	*aggprocoid = funcid;
 
 	/* Look up the function's rettype. */
 	aggrettype = get_func_rettype(funcid);
@@ -1659,8 +1661,16 @@ FindFKPeriodOpersAndProcs(Oid opclass,
 	strat = RTContainedByStrategyNumber;
 	GetOperatorFromWellKnownStrategy(opclass,
 									 aggrettype,
-									 aggedperiodoperoid,
+									 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.
+	 */
+	*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 04bd325e7ac..e4bf02b5163 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -513,7 +513,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,
@@ -9832,6 +9832,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	int16		fkdelsetcols[INDEX_MAX_KEYS] = {0};
 	bool		is_temporal;
 	bool		pk_with_period;
+	int16		fkperiodattnum = InvalidOid;
 	int			i;
 	int			numfks,
 				numpks,
@@ -9933,12 +9934,13 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			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];
 	}

@@ -10056,9 +10058,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	{
 		Oid			periodoperoid;
 		Oid			aggedperiodoperoid;
-		Oid			periodprocoid;
+		Oid			aggprocoid;
+		Oid			intersectprocoid;
 
-		FindFKPeriodOpersAndProcs(opclasses[numpks - 1], &periodoperoid, &aggedperiodoperoid, &periodprocoid);
+		FindFKPeriodOpersAndProcs(opclasses[numpks - 1],
+								  &periodoperoid,
+								  &aggedperiodoperoid,
+								  &aggprocoid,
+								  &intersectprocoid);
 	}
 
 	/*
@@ -10112,6 +10119,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)
 {
@@ -10122,6 +10130,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;
@@ -12697,12 +12712,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 				fk_trigger->funcname = SystemFuncName("RI_FKey_period_restrict_del");
 				break;
 			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_cascade_del");
+				break;
 			case FKCONSTR_ACTION_SETNULL:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setnull_del");
+				break;
 			case FKCONSTR_ACTION_SETDEFAULT:
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("invalid %s action for foreign key constraint using PERIOD",
-								"ON DELETE")));
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setdefault_del");
 				break;
 			default:
 				elog(ERROR, "unrecognized FK action type: %d",
@@ -12789,12 +12811,19 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
 				fk_trigger->funcname = SystemFuncName("RI_FKey_period_restrict_upd");
 				break;
 			case FKCONSTR_ACTION_CASCADE:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_cascade_upd");
+				break;
 			case FKCONSTR_ACTION_SETNULL:
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_setnull_upd");
+				break;
 			case FKCONSTR_ACTION_SETDEFAULT:
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("invalid %s action for foreign key constraint using PERIOD",
-								"ON UPDATE")));
+				fk_trigger->deferrable = false;
+				fk_trigger->initdeferred = false;
+				fk_trigger->funcname = SystemFuncName("RI_FKey_period_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 6a9d35f35fd..6bdc261f274 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -83,6 +83,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)
@@ -131,6 +137,7 @@ typedef struct RI_ConstraintInfo
 	Oid			agged_period_contained_by_oper;	/* operator for PERIOD SQL */
 	Oid			period_referenced_agg_proc;	/* proc for PERIOD SQL */
 	Oid			period_referenced_agg_rettype;	/* rettype for previous */
+	Oid			period_intersect_proc;	/* operator for PERIOD SQL */
 	dlist_node	valid_link;		/* Link in list of valid entries */
 } RI_ConstraintInfo;
 
@@ -194,6 +201,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,
@@ -242,7 +250,7 @@ static void ri_ReportViolation(const RI_ConstraintInfo *riinfo,
 static void lookupPeriodRIProc(const RI_ConstraintInfo *riinfo, 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,
+static Datum restrict_cascading_range(Relation rel, const ForPortionOfState *tg_temporal,
 									  const RI_ConstraintInfo *riinfo,
 									  TupleTableSlot *oldslot);
 
@@ -826,7 +834,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,
@@ -1409,7 +1417,128 @@ RI_FKey_period_restrict_del(PG_FUNCTION_ARGS)
 }
 
 /*
- * TRI_FKey_noaction_upd -
+ * 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);
+		appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s FROM pg_catalog.lower($%d) TO pg_catalog.upper($%d)",
+ * RI_FKey_period_noaction_upd -
  * Give an error and roll back the current transaction if the
  * update has resulted in a violation of the given referential
@@ -1445,6 +1574,415 @@ RI_FKey_period_restrict_upd(PG_FUNCTION_ARGS)
 
+/*
+ * 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);
+		appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM pg_catalog.lower($%d) TO pg_catalog.upper($%d) SET",
+ * 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;
+		appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM pg_catalog.lower($%d) TO pg_catalog.upper($%d) SET",
@@ -2426,7 +2964,8 @@ ri_LoadConstraintInfo(Oid constraintOid)
 		FindFKPeriodOpersAndProcs(opclass,
 								  &riinfo->period_contained_by_oper,
 								  &riinfo->agged_period_contained_by_oper,
-								  &riinfo->period_referenced_agg_proc);
+								  &riinfo->period_referenced_agg_proc,
+								  &riinfo->period_intersect_proc);
 		riinfo->period_referenced_agg_rettype = get_func_rettype(riinfo->period_referenced_agg_proc);
 	}
 
@@ -2582,8 +3121,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
@@ -2618,8 +3157,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
 	{
@@ -3288,8 +3829,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_RI_FKEY_PERIOD_CASCADE_DEL:
+		case F_RI_FKEY_PERIOD_CASCADE_UPD:
 		case F_RI_FKEY_PERIOD_RESTRICT_DEL:
 		case F_RI_FKEY_PERIOD_RESTRICT_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:
 		case F_RI_FKEY_PERIOD_NOACTION_DEL:
 		case F_RI_FKEY_PERIOD_NOACTION_UPD:
 			return RI_TRIGGER_PK;
@@ -3360,7 +3907,7 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn
 /*
  * restrict_cascading_range -
  *
- * Returns a Datum of RangeTypeP holding the appropriate timespan
+ * Returns a Datum 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,
@@ -3368,22 +3915,22 @@ fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintIn
  * trim down the parent's 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 e4f4c2ffaf1..538e8e64466 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -284,9 +284,10 @@ extern void DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
 									   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 FindFKPeriodOpersAndProcs(Oid opclass,
-									  Oid *periodoperoid,
-									  Oid *aggedperiodoperoid,
-									  Oid *periodprocoid);
+									  Oid *containedbyodoperoid,
+									  Oid *aggedcontainedbyoperoid,
+									  Oid *aggprocoid,
+									  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 7fe2542f058..fe01f213349 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 => 'RI_FKey_period_check_upd', provolatile => 'v', prorettype => 'trigger',
   proargtypes => '', prosrc => 'RI_FKey_period_check_upd' },
+{ 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 => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT',
   proname => 'RI_FKey_period_restrict_del', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
@@ -3960,6 +3966,20 @@
   proname => 'RI_FKey_period_restrict_upd', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
   prosrc => 'RI_FKey_period_restrict_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 => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION',
   proname => 'RI_FKey_period_noaction_del', provolatile => 'v',
   prorettype => 'trigger', proargtypes => '',
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index f3595d4ea3b..11002e91709 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -206,16 +206,19 @@ 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_noaction_del
  RI_FKey_period_restrict_del
+ RI_FKey_period_setdefault_del
+ RI_FKey_period_setnull_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(7 rows)
+(10 rows)
 
 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
@@ -254,16 +257,19 @@ 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_noaction_del
  RI_FKey_period_restrict_del
+ RI_FKey_period_setdefault_del
+ RI_FKey_period_setnull_del
  RI_FKey_restrict_del
  RI_FKey_setdefault_del
  RI_FKey_setnull_del
-(7 rows)
+(10 rows)
 
 explain (costs off)
 select proname from pg_proc where proname ilike '00%foo' order by 1;
index 0c8224a35f5..15f35dfe952 100644
@@ -872,6 +872,21 @@ SET id = '[7,7]'
 WHERE id = '[5,5]';
 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".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   |                        valid_at                         
+-------+---------------------------------------------------------
+ [5,6) | ["Mon Feb 01 00:00:00 2016","Tue Mar 01 00:00:00 2016")
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 02 00:00:00 2018")
+ [5,6) | ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")
+ [7,8) | ["Tue Jan 02 00:00:00 2018","Wed Jan 03 00:00:00 2018")
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+ [3,4) | ["Fri Jan 05 00:00:00 2018","Wed Jan 10 00:00:00 2018") | [5,6)
@@ -922,6 +937,21 @@ SET id = '[7,7]'
 WHERE id = '[5,5]';
 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".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   |                        valid_at                         
+-------+---------------------------------------------------------
+ [5,6) | ["Mon Feb 01 00:00:00 2016","Tue Mar 01 00:00:00 2016")
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 02 00:00:00 2018")
+ [5,6) | ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")
+ [7,8) | ["Tue Jan 02 00:00:00 2018","Wed Jan 03 00:00:00 2018")
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+ [3,4) | ["Fri Jan 05 00:00:00 2018","Wed Jan 10 00:00:00 2018") | [5,6)
@@ -961,6 +991,19 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
 WHERE id = '[5,5]';
 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".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   |                        valid_at                         
+-------+---------------------------------------------------------
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 02 00:00:00 2018")
+ [5,6) | ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+ [3,4) | ["Fri Jan 05 00:00:00 2018","Wed Jan 10 00:00:00 2018") | [5,6)
@@ -999,6 +1042,19 @@ FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10'
 WHERE id = '[5,5]';
 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".
+SELECT * FROM temporal_rng WHERE id in ('[5,5]', '[7,7]') ORDER BY id, valid_at;
+  id   |                        valid_at                         
+-------+---------------------------------------------------------
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 02 00:00:00 2018")
+ [5,6) | ["Wed Jan 03 00:00:00 2018","Thu Feb 01 00:00:00 2018")
+SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
+ [3,4) | ["Fri Jan 05 00:00:00 2018","Wed Jan 10 00:00:00 2018") | [5,6)
@@ -1008,30 +1064,184 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
@@ -1039,7 +1249,433 @@ ALTER TABLE temporal_fk_rng2rng
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
 -- test FOREIGN KEY, multirange references multirange
@@ -1315,6 +1951,22 @@ UPDATE temporal_mltrng SET id = '[7,7]'
 WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+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), {[01-01-2018,01-02-2018),[01-03-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
@@ -1349,6 +2001,22 @@ UPDATE temporal_mltrng SET id = '[7,7]'
 WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+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), {[01-01-2018,01-02-2018),[01-03-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
@@ -1374,6 +2042,19 @@ DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(dat
 DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+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), {[01-01-2018,01-02-2018),[01-03-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
@@ -1399,9 +2080,50 @@ DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(dat
 DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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), {[01-01-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03')))
+DELETE FROM temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10')))
+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), {[01-01-2018,01-02-2018),[01-03-2018,02-01-2018)}) is still referenced from table "temporal_fk_mltrng2mltrng".
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
@@ -1409,8 +2131,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -1423,8 +2145,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (
@@ -1459,7 +2181,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
-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"
@@ -1471,7 +2193,7 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-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"
@@ -1493,7 +2215,7 @@ UPDATE temporal_partitioned_rng SET valid_at = daterange('2016-02-01', '2016-03-
-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"
@@ -1505,35 +2227,214 @@ INSERT INTO temporal_partitioned_fk_rng2rng VALUES ('[3,3]', daterange('2018-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"
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
-ERROR:  invalid ON DELETE action for foreign key constraint using PERIOD
index c111f6a6bca..959e0b64267 100644
@@ -664,6 +664,8 @@ UPDATE 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_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
@@ -710,6 +712,8 @@ UPDATE 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_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
@@ -745,6 +749,8 @@ WHERE id = '[5,5]';
 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_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
@@ -780,6 +786,8 @@ WHERE id = '[5,5]';
 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_rng2rng WHERE id in ('[3,3]') ORDER BY id, valid_at;
@@ -790,31 +798,114 @@ DELETE FROM temporal_rng WHERE id IN ('[5,5]');

 
@@ -822,6 +913,245 @@ ALTER TABLE temporal_fk_rng2rng
 
 --
 -- test FOREIGN KEY, multirange references multirange
@@ -1068,6 +1398,20 @@ WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-0
 -- changing the scalar part fails:
 UPDATE temporal_mltrng SET id = '[7,7]'
 WHERE id = '[5,5]' 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '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_mltrng2mltrng WHERE id = '[3,3]';
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 -- clean up:
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
@@ -1100,6 +1444,20 @@ WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-0
 -- changing the scalar part fails:
 UPDATE temporal_mltrng SET id = '[7,7]'
 WHERE id = '[5,5]' 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,7]'
+WHERE id = '[5,5]';
+-- changing just a part fails:
+UPDATE temporal_mltrng
+FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '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_mltrng2mltrng WHERE id = '[3,3]';
+UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01'))
+WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 -- clean up:
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]', '[7,7]');
@@ -1123,6 +1481,17 @@ INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange(
 DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- 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,5]';
+-- 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,5]';
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 -- clean up:
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');
@@ -1147,10 +1516,47 @@ INSERT INTO temporal_fk_mltrng2mltrng VALUES ('[3,3]', datemultirange(daterange(
 DELETE FROM temporal_mltrng WHERE id = '[5,5]' 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,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
+-- 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,5]';
+-- 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,5]';
+-- then delete the objecting FK record and the same PK delete succeeds:
+DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,3]';
+DELETE FROM temporal_mltrng WHERE id = '[5,5]' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01'));
 -- clean up:
 DELETE FROM temporal_fk_mltrng2mltrng WHERE parent_id = '[5,5]';
 DELETE FROM temporal_mltrng WHERE id IN ('[5,5]');

@@ -1159,8 +1565,8 @@ CREATE TABLE temporal_partitioned_rng (
@@ -1174,8 +1580,8 @@ CREATE TABLE temporal_partitioned_fk_rng2rng (

@@ -1259,37 +1665,107 @@ DELETE FROM temporal_partitioned_rng WHERE id = '[5,5]' AND valid_at = daterange

 

 

 

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

 doc/src/sgml/ref/create_table.sgml            |  61 +-
 src/backend/catalog/dependency.c              |  17 +
 src/backend/commands/tablecmds.c              | 860 +++++++++++++++++-
 src/backend/parser/parse_utilcmd.c            | 128 ++-
 src/bin/pg_dump/pg_dump.c                     | 175 +++-
 src/bin/pg_dump/pg_dump.h                     |  15 +
 src/include/nodes/parsenodes.h                |  40 +-
 src/test/regress/expected/periods.out         | 267 ++++++
 src/test/regress/sql/periods.sql              | 178 ++++
 52 files changed, 2641 insertions(+), 46 deletions(-)

index 6ce6d0a5011..afef655856c 100644
 
+     <row>
index 9d7e2c756be..0bc9487f4b1 100644

 

index 3a791c89c33..e5d6a172bd7 100644

 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
@@ -81,7 +89,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 ]
@@ -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>
@@ -800,6 +816,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>
@@ -1152,8 +1199,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>
@@ -1173,18 +1220,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      </para>
 
      <para>
-      If the last column is marked with <literal>PERIOD</literal>,
+      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 treated normally
       (and there must be at least one of them),
-      the <literal>PERIOD</literal> column is not compared for equality.
+      the <literal>PERIOD</literal> part is not compared for equality.
       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.
-      Normally this column would be a range or multirange type,
+      Normally this part would be either a
+      <link linkend="sql-createtable-parms-period"><literal>PERIOD</literal></link>
+      or a column with a range or multirange type,
       although any type whose GiST opclass has a "contained by" operator
       and a <literal>referenced_agg</literal> support function is allowed.
       (See <xref linkend="gist-extensibility"/>.)
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>
 
   <para>
index 196ecafc909..44f0ff63ced 100644
index 1e44a71f61c..e2619f52e01 100644
@@ -2823,6 +2823,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
@@ -2964,6 +2965,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
index 2eb41d537bb..4e52e5a6fc5 100644
@@ -624,6 +625,15 @@ findDependentObjects(const ObjectAddress *object,
 						return;
 					}
 
+					/*
@@ -1396,6 +1406,10 @@ doDeletion(const ObjectAddress *object, int flags)
 
+		case OCLASS_PERIOD:
@@ -2812,6 +2826,9 @@ getObjectClass(const ObjectAddress *object)
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+
 
index 5e773740f4d..aea94e0bcdf 100644
@@ -2044,6 +2045,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;
+
index 925d15a2c38..c93c69750ab 100644
@@ -448,7 +448,7 @@ T176	Sequence generator support			NO	supported except for NEXT VALUE FOR
 
 		case OCLASS_CAST:
 		case OCLASS_CONSTRAINT:
+		case OCLASS_PERIOD:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
 		case OCLASS_LARGEOBJECT:
index c8b662131c3..c0c1c3429b0 100644
@@ -1166,6 +1166,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
@@ -1224,6 +1225,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
@@ -2265,6 +2267,7 @@ stringify_grant_objtype(ObjectType objtype)
@@ -2349,6 +2352,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
index e4bf02b5163..fcd65b7e162 100644
@@ -360,6 +366,7 @@ static void RangeVarCallbackForTruncate(const RangeVar *relation,
 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);
@@ -440,6 +447,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab,
+											bool colexists, bool if_not_exists);
@@ -459,6 +468,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName,

 /* ----------------------------------------------------------------
@@ -903,6 +922,79 @@ 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 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 in-descriptor NOT NULL flags, but
@@ -1288,6 +1380,21 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	foreach(listptr, nncols)
 		set_attnotnull(NULL, rel, lfirst_int(listptr), false, NoLock);
 
+	/*
 
 	/*
@@ -1404,6 +1511,309 @@ BuildDescForRelation(const List *columns)
 	return desc;
 }
 
+/*
+ * 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);
@@ -3125,6 +3535,168 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 }

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

 
 /*
@@ -4564,6 +5136,8 @@ AlterTableGetLockLevel(List *cmds)
 
@@ -4880,6 +5454,14 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -5287,6 +5869,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
@@ -6434,6 +7024,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -6459,6 +7051,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
@@ -7432,14 +8026,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
@@ -7483,6 +8092,77 @@ check_for_column_name_collision(Relation rel, const char *colname,
 	return true;
 }
 
+/*
+								bool colexists, bool if_not_exists)
+		if (!colexists)
@@ -8134,6 +8814,157 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum,
 	return address;
 }
 
+/*
+	(void) check_for_period_name_collision(rel, period->periodname, period->colexists, false);
+	if (!period->colexists)
@@ -14124,6 +14955,16 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 
+			case OCLASS_PERIOD:
@@ -14188,6 +15029,15 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
@@ -16000,7 +16850,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt)

 

 
 		/*
index ca75eec7b75..3320a54f0b2 100644

+
 TypedTableElement:

+TablePeriod:
@@ -7168,6 +7201,14 @@ CommentStmt:

index c7efd8d8cee..f998906fdba 100644

+			case T_PeriodDef:
 
@@ -922,6 +932,96 @@ 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;
@@ -1012,6 +1112,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
@@ -1064,6 +1165,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 	}

 	/*
@@ -1073,10 +1175,18 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 
+		/*
@@ -3017,6 +3127,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.)
@@ -3474,6 +3588,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;
@@ -3536,6 +3651,15 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt,
 						 (int) nodeTag(cmd->def));
 				break;
 
+			case AT_AddPeriod:
+				{
index 53e706c6c01..d93f8f4b96f 100644

 /*
@@ -3508,6 +3571,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 d97ebaff5b8..0e8355d659e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3519,6 +3519,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 10cbf02bebd..c5ab1b7ad8b 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6619,6 +6619,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;
@@ -6696,6 +6697,14 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
+	/* In PG17 upwards we have PERIODs. */
@@ -6833,6 +6842,7 @@ getTables(Archive *fout, int *numTables)
@@ -6916,6 +6926,7 @@ getTables(Archive *fout, int *numTables)
@@ -8580,7 +8591,7 @@ getTransforms(Archive *fout, int *numTransforms)
@@ -8633,6 +8644,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -8647,7 +8660,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9186,15 +9200,36 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

@@ -9216,6 +9251,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			Oid			conrelid = atooid(PQgetvalue(res, j, i_conrelid));
 			TableInfo  *tbinfo = NULL;
 			int			numcons;
+			int			ndumpablechecks;
 
@@ -9235,12 +9271,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
@@ -9299,6 +9336,80 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+			Assert(fout->remoteVersion >= 170000);
+				"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 "
@@ -10569,6 +10680,8 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
@@ -16107,6 +16220,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)",
@@ -16115,7 +16255,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16333,7 +16473,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
@@ -16635,7 +16775,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		dumpTableSecLabel(fout, tbinfo, reltypename);

@@ -18604,6 +18744,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 f49eb88dcbf..183f77671d6 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 */
@@ -355,6 +359,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 */
 
@@ -485,6 +490,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 c05befbb6f2..fd712e579fe 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1943,6 +1943,8 @@ describeOneTableDetails(const char *schemaname,

 
+		/* print periods */
index 436b081738b..45623e86a45 100644
 
index 6b3c56c20e8..ea98e03f5dc 100644

 
 /*
index 60d9b8f5b5f..7ce32878e4b 100644

 

 #endif							/* TABLECMDS_H */
index a20b39a4a28..d111e128b67 100644
 
@@ -2522,6 +2526,31 @@ typedef struct CreateStmt
 
+
+/* ----------
+	bool		colexists;		/* use an existing GENERATED column */
@@ -3217,6 +3246,7 @@ typedef struct IndexStmt
 
index 48df107c1ce..cd9fe5c988d 100644
@@ -196,6 +198,7 @@ extern Oid	get_range_subtype(Oid rangeOid);
 
 \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/parallel_schedule b/src/test/regress/parallel_schedule
index 4fd7f3d0c73..6685cc357df 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..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;
-- 
2.42.0

---

From: Paul Jungwirth <[email-masked]>
Date: Thu, 29 Feb 2024 14:10:49 -0800

On 2/13/24 21:00, jian he wrote:
> opening curly brace should be the next line,

Fixed in v25 (submitted in my other email).

> also do you think it's

That branch doesn't exist anymore. Same with the increments.

> FindFKComparisonOperators(fkconstraint,

There are places we do that, but most code I've seen tries to fill the line. I haven't followed that 
strictly here, but I'm trying to get better at doing what pg_indent wants.

Yours,

---

From: Paul Jungwirth <[email-masked]>
Date: Fri, 1 Mar 2024 12:38:27 -0800

On 2/29/24 13:16, Paul Jungwirth wrote:
> Hello,
> 
> Here is another patch series for application time.
Here is a v26 patch series to fix a cfbot failure in sepgsql. Rebased to 655dc31046.

Yours,

From 24c23d0696d98502d940b68f4dcf5cd47588e541 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Tue, 20 Feb 2024 19:35:23 -0800
Subject: [PATCH v26 1/8] Rename conwithoutoverlaps to conperiod

---

 	pfree(ccbin);
 
 	if (conkeyArray)

 

 
 
 	/*

 

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

 
  <para>
 
  <variablelist>
 
   <para>
 
   <para>
 
+bool

 	va_start(ap, maxargs);

 /*
 
 # gin

From 623c4d70446c4e5fba48cbf73fd82ce818bc77f7 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email-masked]>
Date: Wed, 28 Feb 2024 21:45:37 -0800
Subject: [PATCH v26 3/8] Refactor FK operator lookup

 
 
 	/*
 
+/*

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

 

 
 
+/*

 	*opid = InvalidOid;

 			ereport(ERROR,

 	if (!OidIsValid(*opid))
 
 		ereport(ERROR,

 
 				/*

 	/*
 
+	/*

 	/*

 
 
 			/*

 

 

-	/*
 
 	/*

 	/*

 	return i;
 
 		/*
 
 			/*
 
 	/*
 
 	/*

 

 

+optionalPeriodName:

 /*

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

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

 	return RI_TRIGGER_NONE;
 }
+
+/*
+ * lookupPeriodRIProc -
+ *

 

 

 
 	return nKeys;
 
 	/*

 	/*
 
 	/*
 
 	/*

 
 
index d60e148ff26..94f5c221f64 100644
@@ -2609,6 +2609,8 @@ typedef struct Constraint

 explain (costs off)

 explain (costs off)

 
-

 
 
+-- okay:

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

 
  <para>
 
  <variablelist>

   <para>

 
 /*
 
+/*

 
 Datum
 
 /*
 
 # gin

+-- without_portion

+-- without_portion

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

 contrib/sepgsql/proc.c                        |   2 +-
 51 files changed, 2735 insertions(+), 69 deletions(-)

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

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

 

 

 

 

+/* ----------
+ * ForPortionOfState()
+ *
 
index 79ef46f2614..f49d58b697d 100644
@@ -1251,6 +1251,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
index ff7ec8419bb..daa7fa6324f 100644

@@ -140,6 +142,10 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
@@ -161,6 +167,7 @@ static bool ExecMergeMatched(ModifyTableContext *context,

 /*
@@ -1206,6 +1213,169 @@ ExecInsert(ModifyTableContext *context,
 	return result;
 }
 
@@ -1358,7 +1528,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
@@ -1391,6 +1562,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
@@ -1771,7 +1947,11 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
 
@@ -2137,6 +2317,11 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
 
@@ -4365,6 +4550,117 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*

 

 
 #include "postgres.h"

@@ -60,10 +65,16 @@

+static Node *

 

+/*
 
 /*

 

 
+		/*

 
 	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:
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 
 			/*
 			 * 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 4356d61f8ed..1d4b0731907 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -376,7 +376,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 b8839b56b7a..410949a99e2 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3678,6 +3678,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,
@@ -4016,6 +4040,22 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)

 /*

 

 

 	/*

+/*
+ * get_typname
+ *

 #endif							/* STRATNUM_H */
 
 /*

+/*
+ * ForPortionOfState
+ *

index 94f5c221f64..e1b9e76f56c 100644
 
@@ -1538,6 +1541,21 @@ typedef struct RowMarkClause
 
+/*
+ * ForPortionOfClause
@@ -1901,12 +1919,13 @@ typedef struct InsertStmt
 
@@ -1915,13 +1934,14 @@ typedef struct DeleteStmt

+/*----------
 
index 794cf9cf930..b5be5535ccd 100644
@@ -3544,6 +3544,38 @@ select * from uv_iocu_tab;
 
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 # ----------

index ae11e46da28..4427db67121 100644
@@ -1824,6 +1824,20 @@ select * from uv_iocu_tab;
 drop view uv_iocu_view;
 drop table uv_iocu_tab;

 
 -- temporal UNIQUE:

 

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

---

          <para>
 
          <para>
 
          <para>
 
 	/*

 
 
 /*

 
 
 	/*

 

 
 /*
 
+/*

 	/*

 

 explain (costs off)

 explain (costs off)

 

 

 

 

 

 

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

 
+     <row>

 

 

+  <para>

 
      <para>
 
   <para>
 
   <para>
 
+					/*
 
+		case OCLASS_PERIOD:
 
+		case PeriodRelationId:
+			return OCLASS_PERIOD;
+

+/*

 /*
 
+		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:

+	/*
 
+	/*
 
 	/*
 
+/*

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

 
 /*

 
+/*
 
+/*
 
+			case OCLASS_PERIOD:

 

 
 		/*

+
 TypedTableElement:

+TablePeriod:

 
 
+			case T_PeriodDef:

+void

 	/*
 
+		/*

+			case AT_AddPeriod:
+				{

 /*
 
+Oid
 
 /*

 
 
 		resetPQExpBuffer(q);
-		appendPQExpBuffer(q,

 

 
+			/*

 

 

 
+		/* print periods */

 
 
 /*

 

 #endif							/* TABLECMDS_H */
index e1b9e76f56c..a6d74c41c00 100644
@@ -2149,6 +2149,7 @@ typedef enum ObjectType
@@ -2237,6 +2238,8 @@ typedef enum AlterTableType
@@ -2503,11 +2506,11 @@ typedef struct VariableShowStmt
 
@@ -2516,6 +2519,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 */
@@ -2530,6 +2534,31 @@ typedef struct CreateStmt
 	bool		if_not_exists;	/* just do nothing if it already exists? */
 } CreateStmt;
 
+
+/* ----------
@@ -3225,6 +3254,7 @@ typedef struct IndexStmt

 \d gtest1
                             Table "public.gtest1"
 
 # ----------

---

From: Paul A Jungwirth <[email-masked]>
Date: Sun, 15 Sep 2024 21:12:34 -0500

> suitable performance?  Do you have example execution plans maybe?

This took longer than expected, but I wrote a long blog post about it
here: https://illuminatedcomputing.com/posts/2024/09/benchmarking-temporal-foreign-keys/

The short answer is that yes we use the index, and the query plan is
reasonable. I compared performance against two alternate
implementations, and range_agg was fastest most of the time. When you
have a lot of invalid FK checks, the implementation in Snodgrass's
book wins, because it can short-circuit the plan and return a false
result without executing most of it. But that seems like an unusual
situation, and we should optimize for mostly-valid FK checks instead.

There are some more experiments I'd like to do (see the end of that
post), but for now I plan to prioritize getting the FOR PORTION OF
patch ready to commit. But if there is anything you'd like to know
more urgently, let me know.

Yours,