Skip to content

Commit 89f908a

Browse files
committed
Add temporal FOREIGN KEY contraints
Add PERIOD clause to foreign key constraint definitions. This is supported for range and multirange types. Temporal foreign keys check for range containment instead of equality. This feature matches the behavior of the SQL standard temporal foreign keys, but it works on PostgreSQL's native ranges instead of SQL's "periods", which don't exist in PostgreSQL (yet). Reference actions ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT} are not supported yet. (previously committed as 34768ee, reverted by 8aee330; this is essentially unchanged from those) Author: Paul A. Jungwirth <[email protected]> Reviewed-by: Peter Eisentraut <[email protected]> Reviewed-by: jian he <[email protected]> Discussion: https://www.postgresql.org/message-id/flat/CA+renyUApHgSZF9-nd-a0+OPGharLQLO=mDHcY4_qQ0+noCUVg@mail.gmail.com
1 parent fc0438b commit 89f908a

File tree

16 files changed

+3105
-108
lines changed

16 files changed

+3105
-108
lines changed

contrib/btree_gist/expected/without_overlaps.out

+48
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,51 @@ INSERT INTO temporal_rng VALUES
4242
(1, '[2000-06-01,2001-01-01)');
4343
ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk"
4444
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)).
45+
-- Foreign key
46+
CREATE TABLE temporal_fk_rng2rng (
47+
id integer,
48+
valid_at daterange,
49+
parent_id integer,
50+
CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
51+
CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
52+
REFERENCES temporal_rng (id, PERIOD valid_at)
53+
);
54+
\d temporal_fk_rng2rng
55+
Table "public.temporal_fk_rng2rng"
56+
Column | Type | Collation | Nullable | Default
57+
-----------+-----------+-----------+----------+---------
58+
id | integer | | not null |
59+
valid_at | daterange | | not null |
60+
parent_id | integer | | |
61+
Indexes:
62+
"temporal_fk_rng2rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
63+
Foreign-key constraints:
64+
"temporal_fk_rng2rng_fk" FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
65+
66+
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
67+
pg_get_constraintdef
68+
---------------------------------------------------------------------------------------
69+
FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng(id, PERIOD valid_at)
70+
(1 row)
71+
72+
-- okay
73+
INSERT INTO temporal_fk_rng2rng VALUES
74+
(1, '[2000-01-01,2001-01-01)', 1);
75+
-- okay spanning two parent records:
76+
INSERT INTO temporal_fk_rng2rng VALUES
77+
(2, '[2000-01-01,2002-01-01)', 1);
78+
-- key is missing
79+
INSERT INTO temporal_fk_rng2rng VALUES
80+
(3, '[2000-01-01,2001-01-01)', 3);
81+
ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
82+
DETAIL: Key (parent_id, valid_at)=(3, [01-01-2000,01-01-2001)) is not present in table "temporal_rng".
83+
-- key exist but is outside range
84+
INSERT INTO temporal_fk_rng2rng VALUES
85+
(4, '[2001-01-01,2002-01-01)', 2);
86+
ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
87+
DETAIL: Key (parent_id, valid_at)=(2, [01-01-2001,01-01-2002)) is not present in table "temporal_rng".
88+
-- key exist but is partly outside range
89+
INSERT INTO temporal_fk_rng2rng VALUES
90+
(5, '[2000-01-01,2002-01-01)', 2);
91+
ERROR: insert or update on table "temporal_fk_rng2rng" violates foreign key constraint "temporal_fk_rng2rng_fk"
92+
DETAIL: Key (parent_id, valid_at)=(2, [01-01-2000,01-01-2002)) is not present in table "temporal_rng".

contrib/btree_gist/sql/without_overlaps.sql

+28
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,31 @@ INSERT INTO temporal_rng VALUES
2323
-- should fail:
2424
INSERT INTO temporal_rng VALUES
2525
(1, '[2000-06-01,2001-01-01)');
26+
27+
-- Foreign key
28+
CREATE TABLE temporal_fk_rng2rng (
29+
id integer,
30+
valid_at daterange,
31+
parent_id integer,
32+
CONSTRAINT temporal_fk_rng2rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS),
33+
CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at)
34+
REFERENCES temporal_rng (id, PERIOD valid_at)
35+
);
36+
\d temporal_fk_rng2rng
37+
SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_fk_rng2rng_fk';
38+
39+
-- okay
40+
INSERT INTO temporal_fk_rng2rng VALUES
41+
(1, '[2000-01-01,2001-01-01)', 1);
42+
-- okay spanning two parent records:
43+
INSERT INTO temporal_fk_rng2rng VALUES
44+
(2, '[2000-01-01,2002-01-01)', 1);
45+
-- key is missing
46+
INSERT INTO temporal_fk_rng2rng VALUES
47+
(3, '[2000-01-01,2001-01-01)', 3);
48+
-- key exist but is outside range
49+
INSERT INTO temporal_fk_rng2rng VALUES
50+
(4, '[2001-01-01,2002-01-01)', 2);
51+
-- key exist but is partly outside range
52+
INSERT INTO temporal_fk_rng2rng VALUES
53+
(5, '[2000-01-01,2002-01-01)', 2);

doc/src/sgml/catalogs.sgml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2736,7 +2736,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
27362736
</para>
27372737
<para>
27382738
This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
2739-
(for primary keys and unique constraints).
2739+
(for primary keys and unique constraints) or <literal>PERIOD</literal>
2740+
(for foreign keys).
27402741
</para></entry>
27412742
</row>
27422743

doc/src/sgml/ref/create_table.sgml

+41-4
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
8080
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
8181
PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
8282
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> ) ] |
83-
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
83+
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> ] ) ]
8484
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
8585
class="parameter">referential_action</replaceable> ] [ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ] }
8686
[ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ]
@@ -1147,8 +1147,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
11471147
<varlistentry id="sql-createtable-parms-references">
11481148
<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>
11491149

1150-
<term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
1151-
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
1150+
<term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] )
1151+
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
11521152
[ MATCH <replaceable class="parameter">matchtype</replaceable> ]
11531153
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
11541154
[ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1164,7 +1164,32 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
11641164
primary key of the <replaceable class="parameter">reftable</replaceable>
11651165
is used. Otherwise, the <replaceable class="parameter">refcolumn</replaceable>
11661166
list must refer to the columns of a non-deferrable unique or primary key
1167-
constraint or be the columns of a non-partial unique index. The user
1167+
constraint or be the columns of a non-partial unique index.
1168+
</para>
1169+
1170+
<para>
1171+
If the last column is marked with <literal>PERIOD</literal>, it is
1172+
treated in a special way. While the non-<literal>PERIOD</literal>
1173+
columns are compared for equality (and there must be at least one of
1174+
them), the <literal>PERIOD</literal> column is not. Instead, the
1175+
constraint is considered satisfied if the referenced table has matching
1176+
records (based on the non-<literal>PERIOD</literal> parts of the key)
1177+
whose combined <literal>PERIOD</literal> values completely cover the
1178+
referencing record's. In other words, the reference must have a
1179+
referent for its entire duration. This column must be a range or
1180+
multirange type. In addition, the referenced table must have a primary
1181+
key or unique constraint declared with <literal>WITHOUT
1182+
OVERLAPS</literal>. Finally, if the foreign key has a PERIOD
1183+
<replaceable class="parameter">column_name</replaceable> specification
1184+
the corresponding <replaceable class="parameter">refcolumn</replaceable>,
1185+
if present, must also be marked <literal>PERIOD</literal>. If the
1186+
<replaceable class="parameter">refcolumn</replaceable> clause is omitted,
1187+
and thus the reftable's primary key constraint chosen, the primary key
1188+
must have its final column marked <literal>WITHOUT OVERLAPS</literal>.
1189+
</para>
1190+
1191+
<para>
1192+
The user
11681193
must have <literal>REFERENCES</literal> permission on the referenced
11691194
table (either the whole table, or the specific referenced columns). The
11701195
addition of a foreign key constraint requires a
@@ -1238,6 +1263,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12381263
values of the referencing column(s) to the new values of the
12391264
referenced columns, respectively.
12401265
</para>
1266+
1267+
<para>
1268+
In a temporal foreign key, this option is not supported.
1269+
</para>
12411270
</listitem>
12421271
</varlistentry>
12431272

@@ -1249,6 +1278,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12491278
referencing columns, to null. A subset of columns can only be
12501279
specified for <literal>ON DELETE</literal> actions.
12511280
</para>
1281+
1282+
<para>
1283+
In a temporal foreign key, this option is not supported.
1284+
</para>
12521285
</listitem>
12531286
</varlistentry>
12541287

@@ -1262,6 +1295,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12621295
(There must be a row in the referenced table matching the default
12631296
values, if they are not null, or the operation will fail.)
12641297
</para>
1298+
1299+
<para>
1300+
In a temporal foreign key, this option is not supported.
1301+
</para>
12651302
</listitem>
12661303
</varlistentry>
12671304
</variablelist>

src/backend/catalog/pg_constraint.c

+58
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "postgres.h"
1616

1717
#include "access/genam.h"
18+
#include "access/gist.h"
1819
#include "access/htup_details.h"
1920
#include "access/sysattr.h"
2021
#include "access/table.h"
@@ -1349,6 +1350,63 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
13491350
*numfks = numkeys;
13501351
}
13511352

1353+
/*
1354+
* FindFkPeriodOpers -
1355+
*
1356+
* Looks up the operator oids used for the PERIOD part of a temporal foreign key.
1357+
* The opclass should be the opclass of that PERIOD element.
1358+
* Everything else is an output: containedbyoperoid is the ContainedBy operator for
1359+
* types matching the PERIOD element.
1360+
* aggedcontainedbyoperoid is also a ContainedBy operator,
1361+
* but one whose rhs is a multirange.
1362+
* That way foreign keys can compare fkattr <@ range_agg(pkattr).
1363+
*/
1364+
void
1365+
FindFKPeriodOpers(Oid opclass,
1366+
Oid *containedbyoperoid,
1367+
Oid *aggedcontainedbyoperoid)
1368+
{
1369+
Oid opfamily = InvalidOid;
1370+
Oid opcintype = InvalidOid;
1371+
StrategyNumber strat;
1372+
1373+
/* Make sure we have a range or multirange. */
1374+
if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
1375+
{
1376+
if (opcintype != ANYRANGEOID && opcintype != ANYMULTIRANGEOID)
1377+
ereport(ERROR,
1378+
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1379+
errmsg("invalid type for PERIOD part of foreign key"),
1380+
errdetail("Only range and multirange are supported."));
1381+
1382+
}
1383+
else
1384+
elog(ERROR, "cache lookup failed for opclass %u", opclass);
1385+
1386+
/*
1387+
* Look up the ContainedBy operator whose lhs and rhs are the opclass's
1388+
* type. We use this to optimize RI checks: if the new value includes all
1389+
* of the old value, then we can treat the attribute as if it didn't
1390+
* change, and skip the RI check.
1391+
*/
1392+
strat = RTContainedByStrategyNumber;
1393+
GetOperatorFromWellKnownStrategy(opclass,
1394+
InvalidOid,
1395+
containedbyoperoid,
1396+
&strat);
1397+
1398+
/*
1399+
* Now look up the ContainedBy operator. Its left arg must be the type of
1400+
* the column (or rather of the opclass). Its right arg must match the
1401+
* return type of the support proc.
1402+
*/
1403+
strat = RTContainedByStrategyNumber;
1404+
GetOperatorFromWellKnownStrategy(opclass,
1405+
ANYMULTIRANGEOID,
1406+
aggedcontainedbyoperoid,
1407+
&strat);
1408+
}
1409+
13521410
/*
13531411
* Determine whether a relation can be proven functionally dependent on
13541412
* a set of grouping columns. If so, return true and add the pg_constraint

src/backend/commands/indexcmds.c

+18-13
Original file line numberDiff line numberDiff line change
@@ -2205,7 +2205,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
22052205
strat = RTOverlapStrategyNumber;
22062206
else
22072207
strat = RTEqualStrategyNumber;
2208-
GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
2208+
GetOperatorFromWellKnownStrategy(opclassOids[attn], InvalidOid,
22092209
&opid, &strat);
22102210
indexInfo->ii_ExclusionOps[attn] = opid;
22112211
indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
@@ -2445,7 +2445,7 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
24452445
* GetOperatorFromWellKnownStrategy
24462446
*
24472447
* opclass - the opclass to use
2448-
* atttype - the type to ask about
2448+
* rhstype - the type for the right-hand side, or InvalidOid to use the type of the given opclass.
24492449
* opid - holds the operator we found
24502450
* strat - holds the input and output strategy number
24512451
*
@@ -2458,14 +2458,14 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
24582458
* InvalidStrategy.
24592459
*/
24602460
void
2461-
GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
2461+
GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
24622462
Oid *opid, StrategyNumber *strat)
24632463
{
24642464
Oid opfamily;
24652465
Oid opcintype;
24662466
StrategyNumber instrat = *strat;
24672467

2468-
Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
2468+
Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber || instrat == RTContainedByStrategyNumber);
24692469

24702470
*opid = InvalidOid;
24712471

@@ -2488,16 +2488,21 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
24882488

24892489
ereport(ERROR,
24902490
errcode(ERRCODE_UNDEFINED_OBJECT),
2491-
instrat == RTEqualStrategyNumber ?
2492-
errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
2493-
errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
2491+
instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
2492+
instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
2493+
instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
24942494
errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
24952495
instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
2496-
2497-
ReleaseSysCache(tuple);
24982496
}
24992497

2500-
*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
2498+
/*
2499+
* We parameterize rhstype so foreign keys can ask for a <@ operator
2500+
* whose rhs matches the aggregate function. For example range_agg
2501+
* returns anymultirange.
2502+
*/
2503+
if (!OidIsValid(rhstype))
2504+
rhstype = opcintype;
2505+
*opid = get_opfamily_member(opfamily, opcintype, rhstype, *strat);
25012506
}
25022507

25032508
if (!OidIsValid(*opid))
@@ -2510,9 +2515,9 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
25102515

25112516
ereport(ERROR,
25122517
errcode(ERRCODE_UNDEFINED_OBJECT),
2513-
instrat == RTEqualStrategyNumber ?
2514-
errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
2515-
errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
2518+
instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
2519+
instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
2520+
instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
25162521
errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
25172522
NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
25182523
}

0 commit comments

Comments
 (0)