Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 34768ee

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. Author: Paul A. Jungwirth <pj@illuminatedcomputing.com> Reviewed-by: Peter Eisentraut <peter@eisentraut.org> Reviewed-by: jian he <jian.universality@gmail.com> Discussion: https://www.postgresql.org/message-id/flat/CA+renyUApHgSZF9-nd-a0+OPGharLQLO=mDHcY4_qQ0+noCUVg@mail.gmail.com
1 parent b1fe8ef commit 34768ee

File tree

16 files changed

+2790
-118
lines changed

16 files changed

+2790
-118
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
@@ -2728,7 +2728,8 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
27282728
</para>
27292729
<para>
27302730
This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
2731-
(for primary keys and unique constraints).
2731+
(for primary keys and unique constraints) or <literal>PERIOD</literal>
2732+
(for foreign keys).
27322733
</para></entry>
27332734
</row>
27342735

doc/src/sgml/ref/create_table.sgml

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

1155-
<term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )
1156-
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
1155+
<term><literal>FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] )
1156+
REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] [, PERIOD <replaceable class="parameter">column_name</replaceable> ] ) ]
11571157
[ MATCH <replaceable class="parameter">matchtype</replaceable> ]
11581158
[ ON DELETE <replaceable class="parameter">referential_action</replaceable> ]
11591159
[ ON UPDATE <replaceable class="parameter">referential_action</replaceable> ]</literal>
@@ -1169,7 +1169,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
11691169
primary key of the <replaceable class="parameter">reftable</replaceable>
11701170
is used. Otherwise, the <replaceable class="parameter">refcolumn</replaceable>
11711171
list must refer to the columns of a non-deferrable unique or primary key
1172-
constraint or be the columns of a non-partial unique index. The user
1172+
constraint or be the columns of a non-partial unique index.
1173+
</para>
1174+
1175+
<para>
1176+
If the last column is marked with <literal>PERIOD</literal>, it is
1177+
treated in a special way. While the non-<literal>PERIOD</literal>
1178+
columns are compared for equality (and there must be at least one of
1179+
them), the <literal>PERIOD</literal> column is not. Instead, the
1180+
constraint is considered satisfied if the referenced table has matching
1181+
records (based on the non-<literal>PERIOD</literal> parts of the key)
1182+
whose combined <literal>PERIOD</literal> values completely cover the
1183+
referencing record's. In other words, the reference must have a
1184+
referent for its entire duration. This column must be a range or
1185+
multirange type. In addition, the referenced table must have a primary
1186+
key or unique constraint declared with <literal>WITHOUT
1187+
OVERLAPS</literal>. Finally, if one side of the foreign key uses
1188+
<literal>PERIOD</literal>, the other side must too. If the <replaceable
1189+
class="parameter">refcolumn</replaceable> list is omitted, the
1190+
<literal>WITHOUT OVERLAPS</literal> part of the primary key is treated
1191+
as if marked with <literal>PERIOD</literal>.
1192+
</para>
1193+
1194+
<para>
1195+
The user
11731196
must have <literal>REFERENCES</literal> permission on the referenced
11741197
table (either the whole table, or the specific referenced columns). The
11751198
addition of a foreign key constraint requires a
@@ -1243,6 +1266,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12431266
values of the referencing column(s) to the new values of the
12441267
referenced columns, respectively.
12451268
</para>
1269+
1270+
<para>
1271+
In a temporal foreign key, this option is not supported.
1272+
</para>
12461273
</listitem>
12471274
</varlistentry>
12481275

@@ -1254,6 +1281,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12541281
referencing columns, to null. A subset of columns can only be
12551282
specified for <literal>ON DELETE</literal> actions.
12561283
</para>
1284+
1285+
<para>
1286+
In a temporal foreign key, this option is not supported.
1287+
</para>
12571288
</listitem>
12581289
</varlistentry>
12591290

@@ -1267,6 +1298,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
12671298
(There must be a row in the referenced table matching the default
12681299
values, if they are not null, or the operation will fail.)
12691300
</para>
1301+
1302+
<para>
1303+
In a temporal foreign key, this option is not supported.
1304+
</para>
12701305
</listitem>
12711306
</varlistentry>
12721307
</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"
@@ -1649,6 +1650,63 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks,
16491650
*numfks = numkeys;
16501651
}
16511652

1653+
/*
1654+
* FindFkPeriodOpers -
1655+
*
1656+
* Looks up the operator oids used for the PERIOD part of a temporal foreign key.
1657+
* The opclass should be the opclass of that PERIOD element.
1658+
* Everything else is an output: containedbyoperoid is the ContainedBy operator for
1659+
* types matching the PERIOD element.
1660+
* aggedcontainedbyoperoid is also a ContainedBy operator,
1661+
* but one whose rhs is a multirange.
1662+
* That way foreign keys can compare fkattr <@ range_agg(pkattr).
1663+
*/
1664+
void
1665+
FindFKPeriodOpers(Oid opclass,
1666+
Oid *containedbyoperoid,
1667+
Oid *aggedcontainedbyoperoid)
1668+
{
1669+
Oid opfamily = InvalidOid;
1670+
Oid opcintype = InvalidOid;
1671+
StrategyNumber strat;
1672+
1673+
/* Make sure we have a range or multirange. */
1674+
if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype))
1675+
{
1676+
if (opcintype != ANYRANGEOID && opcintype != ANYMULTIRANGEOID)
1677+
ereport(ERROR,
1678+
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1679+
errmsg("invalid type for PERIOD part of foreign key"),
1680+
errdetail("Only range and multirange are supported."));
1681+
1682+
}
1683+
else
1684+
elog(ERROR, "cache lookup failed for opclass %u", opclass);
1685+
1686+
/*
1687+
* Look up the ContainedBy operator whose lhs and rhs are the opclass's
1688+
* type. We use this to optimize RI checks: if the new value includes all
1689+
* of the old value, then we can treat the attribute as if it didn't
1690+
* change, and skip the RI check.
1691+
*/
1692+
strat = RTContainedByStrategyNumber;
1693+
GetOperatorFromWellKnownStrategy(opclass,
1694+
InvalidOid,
1695+
containedbyoperoid,
1696+
&strat);
1697+
1698+
/*
1699+
* Now look up the ContainedBy operator. Its left arg must be the type of
1700+
* the column (or rather of the opclass). Its right arg must match the
1701+
* return type of the support proc.
1702+
*/
1703+
strat = RTContainedByStrategyNumber;
1704+
GetOperatorFromWellKnownStrategy(opclass,
1705+
ANYMULTIRANGEOID,
1706+
aggedcontainedbyoperoid,
1707+
&strat);
1708+
}
1709+
16521710
/*
16531711
* Determine whether a relation can be proven functionally dependent on
16541712
* 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
@@ -2185,7 +2185,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
21852185
strat = RTOverlapStrategyNumber;
21862186
else
21872187
strat = RTEqualStrategyNumber;
2188-
GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype,
2188+
GetOperatorFromWellKnownStrategy(opclassOids[attn], InvalidOid,
21892189
&opid, &strat);
21902190
indexInfo->ii_ExclusionOps[attn] = opid;
21912191
indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid);
@@ -2425,7 +2425,7 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
24252425
* GetOperatorFromWellKnownStrategy
24262426
*
24272427
* opclass - the opclass to use
2428-
* atttype - the type to ask about
2428+
* rhstype - the type for the right-hand side, or InvalidOid to use the type of the given opclass.
24292429
* opid - holds the operator we found
24302430
* strat - holds the input and output strategy number
24312431
*
@@ -2438,14 +2438,14 @@ GetDefaultOpClass(Oid type_id, Oid am_id)
24382438
* InvalidStrategy.
24392439
*/
24402440
void
2441-
GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
2441+
GetOperatorFromWellKnownStrategy(Oid opclass, Oid rhstype,
24422442
Oid *opid, StrategyNumber *strat)
24432443
{
24442444
Oid opfamily;
24452445
Oid opcintype;
24462446
StrategyNumber instrat = *strat;
24472447

2448-
Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber);
2448+
Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber || instrat == RTContainedByStrategyNumber);
24492449

24502450
*opid = InvalidOid;
24512451

@@ -2468,16 +2468,21 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
24682468

24692469
ereport(ERROR,
24702470
errcode(ERRCODE_UNDEFINED_OBJECT),
2471-
instrat == RTEqualStrategyNumber ?
2472-
errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
2473-
errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
2471+
instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
2472+
instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
2473+
instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
24742474
errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".",
24752475
instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist"));
2476-
2477-
ReleaseSysCache(tuple);
24782476
}
24792477

2480-
*opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat);
2478+
/*
2479+
* We parameterize rhstype so foreign keys can ask for a <@ operator
2480+
* whose rhs matches the aggregate function. For example range_agg
2481+
* returns anymultirange.
2482+
*/
2483+
if (!OidIsValid(rhstype))
2484+
rhstype = opcintype;
2485+
*opid = get_opfamily_member(opfamily, opcintype, rhstype, *strat);
24812486
}
24822487

24832488
if (!OidIsValid(*opid))
@@ -2490,9 +2495,9 @@ GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype,
24902495

24912496
ereport(ERROR,
24922497
errcode(ERRCODE_UNDEFINED_OBJECT),
2493-
instrat == RTEqualStrategyNumber ?
2494-
errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) :
2495-
errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)),
2498+
instrat == RTEqualStrategyNumber ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) :
2499+
instrat == RTOverlapStrategyNumber ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) :
2500+
instrat == RTContainedByStrategyNumber ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0,
24962501
errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".",
24972502
NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist"));
24982503
}

0 commit comments

Comments
 (0)