From 71f94fc368b2d4ef96e3aaaca5e8c42061d9e7b3 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Sun, 27 Jun 2021 12:58:51 -0700 Subject: [PATCH v10 4/4] Add temporal FOREIGN KEYs - Added bison support for temporal FOREIGN KEYs. Like primary keys, they can have either a PERIOD or a range column for the temporal component. - Added temporal trigger functions akin to existing RI trigger functions. - Added support for ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}. - Added pg_constraint.confperiod to record the oid of a PERIOD used in the other table. If an FK uses a range column instead, we just record its attnum with the other attnums used by the constraint. - We support foreign keys where one table uses a PERIOD and the other a range: you don't have to choose one or the other for your whole database. - Added pg_dump support. - Show the correct syntax in psql \d output for foreign keys. - Added tests and documentation. --- doc/src/sgml/ddl.sgml | 4 + doc/src/sgml/ref/create_table.sgml | 40 +- src/backend/catalog/heap.c | 1 + src/backend/catalog/index.c | 1 + src/backend/catalog/pg_constraint.c | 12 +- src/backend/commands/tablecmds.c | 891 +++++--- src/backend/commands/trigger.c | 2 + src/backend/commands/typecmds.c | 1 + src/backend/nodes/copyfuncs.c | 3 + src/backend/parser/gram.y | 31 +- src/backend/utils/adt/rangetypes.c | 29 + src/backend/utils/adt/ri_triggers.c | 1528 ++++++++++++-- src/backend/utils/adt/ruleutils.c | 17 +- src/include/catalog/pg_constraint.h | 10 +- src/include/catalog/pg_proc.dat | 44 + src/include/nodes/parsenodes.h | 2 + src/include/utils/rangetypes.h | 1 + src/test/regress/expected/sanity_check.out | 1 + .../regress/expected/without_overlaps.out | 1846 +++++++++++++++++ src/test/regress/sql/without_overlaps.sql | 1402 +++++++++++++ 20 files changed, 5381 insertions(+), 485 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 57e0dc1363..baa2c101b7 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1192,6 +1192,10 @@ CREATE TABLE billing_addresses ( Any table with a temporal primary key supports temporal UPDATE and DELETE using FOR PORTION OF syntax. See and for details. + + + Temporal foreign keys have the same structure as temporal primary keys and must reference temporal primary keys. The referenced entity specified by the ordinary key parts must exist for at least as long as the value in the referencing row. Note that several rows in the referenced table may be required to completely satisfy a referencing value. + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index fcf15013a7..7d64fc8348 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -88,7 +88,7 @@ PERIOD FOR { period_name | SYSTEM_T UNIQUE ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | PRIMARY KEY ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | - FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ] + FOREIGN KEY ( column_name [, ... ] [, PERIOD temporal_interval ] ) REFERENCES reftable [ ( refcolumn [, ... ] [, PERIOD temporal_interval ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] } [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] @@ -1141,8 +1141,8 @@ WITH ( MODULUS numeric_literal, REM REFERENCES reftable [ ( refcolumn ) ] [ MATCH matchtype ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] (column constraint) - FOREIGN KEY ( column_name [, ... ] ) - REFERENCES reftable [ ( refcolumn [, ... ] ) ] + FOREIGN KEY ( column_name [, ... ] [, PERIOD temporal_interval ] ) + REFERENCES reftable [ ( refcolumn [, ... ] [, PERIOD temporal_interval ] ) ] [ MATCH matchtype ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] @@ -1153,11 +1153,29 @@ WITH ( MODULUS numeric_literal, 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 refcolumn list is omitted, the primary key of the reftable 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. + + + + If the last column is marked with PERIOD, + it must be a period or range column, and the referenced table + must have a temporal primary key. + The non-PERIOD columns are treated normally + (and there must be at least one of them), + but the PERIOD 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. + + + + The user must have REFERENCES permission on the referenced table (either the whole table, or the specific referenced columns). The addition of a foreign key constraint requires a @@ -1229,7 +1247,10 @@ WITH ( MODULUS numeric_literal, REM 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 FOR PORTION OF semantics + to constrain the effect to the bounds of the referenced row + being deleted. @@ -1238,7 +1259,9 @@ WITH ( MODULUS numeric_literal, REM SET NULL - Set the referencing column(s) to null. + Set the referencing column(s) to null. In a temporal foreign key, + the change will use FOR PORTION OF semantics + to constrain the effect to the bounds of the referenced row. @@ -1250,6 +1273,9 @@ WITH ( MODULUS numeric_literal, REM Set the referencing column(s) to their default values. (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 FOR PORTION + OF semantics to constrain the effect to the bounds of the + referenced row. diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index d8b99ade5c..e3b0089df9 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2560,6 +2560,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, is_no_inherit, /* connoinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ is_internal); /* internally constructed? */ pfree(ccbin); diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 68c5700f6c..abffe387b7 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2010,6 +2010,7 @@ index_constraint_create(Relation heapRelation, noinherit, is_temporal, /* contemporal */ periodid, /* conperiod */ + InvalidOid, /* confperiod */ is_internal); /* diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 7927fbde88..43dcdb3acc 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -78,6 +78,7 @@ CreateConstraintEntry(const char *constraintName, bool conNoInherit, bool conTemporal, Oid period, + Oid fperiod, bool is_internal) { Relation conDesc; @@ -190,6 +191,7 @@ CreateConstraintEntry(const char *constraintName, values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit); values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal); values[Anum_pg_constraint_conperiod - 1] = ObjectIdGetDatum(period); + values[Anum_pg_constraint_confperiod - 1] = ObjectIdGetDatum(fperiod); if (conkeyArray) values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray); @@ -291,7 +293,7 @@ CreateConstraintEntry(const char *constraintName, { /* * Register normal dependency from constraint to foreign relation, or - * to specific column(s) if any are mentioned. + * to specific column(s) and period if any are mentioned. */ ObjectAddress relobject; @@ -309,6 +311,14 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(relobject, RelationRelationId, foreignRelId); add_exact_object_address(&relobject, addrs_normal); } + + if (OidIsValid(fperiod)) + { + ObjectAddress periodobject; + + ObjectAddressSet(periodobject, PeriodRelationId, fperiod); + add_exact_object_address(&periodobject, addrs_normal); + } } if (OidIsValid(indexRelId) && constraintType == CONSTRAINT_FOREIGN) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 88b692bd8d..4907cbf1ba 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -367,19 +367,24 @@ static ObjectAddress ATExecValidateConstraint(List **wqueue, bool recurse, bool recursing, LOCKMODE lockmode); static int transformColumnNameList(Oid relId, List *colList, int16 *attnums, Oid *atttypids); +static void transformPeriodName(Oid relId, Node *periodName, + int16 *attnums, Oid *atttypids, Oid *periodid); static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, Oid *periodid, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses); static Oid transformFkeyCheckAttrs(Relation pkrel, int numattrs, int16 *attnums, - Oid *opclasses); + bool is_temporal, int16 *periodattnums, + Oid periodid, 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); @@ -493,12 +498,13 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok); + bool old_check_ok, bool is_temporal, Oid pkperiod, Oid fkperiod); static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok, LOCKMODE lockmode); + bool old_check_ok, bool is_temporal, Oid pkperiod, Oid fkperiod, + LOCKMODE lockmode); static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel, Relation partitionRel); static void CloneFkReferenced(Relation parentRel, Relation partitionRel); @@ -515,6 +521,12 @@ static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, Oid parentConstrOid, int numfks, AttrNumber *mapped_conkey, AttrNumber *confkey, Oid *conpfeqop); +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 recursing, @@ -5831,7 +5843,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 @@ -9503,11 +9516,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Oid pfeqoperators[INDEX_MAX_KEYS]; Oid ppeqoperators[INDEX_MAX_KEYS]; Oid ffeqoperators[INDEX_MAX_KEYS]; + bool is_temporal = (fkconstraint->fk_period != NULL); + int16 pkperiodattnums[2]; + int16 fkperiodattnums[2]; + Oid pkperiodtypoids[2]; + Oid fkperiodtypoids[2]; int i; int numfks, numpks; Oid indexOid; bool old_check_ok; + Oid fkperiod = InvalidOid; + Oid pkperiod = InvalidOid; ObjectAddress address; ListCell *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop); @@ -9605,6 +9625,21 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numfks = transformColumnNameList(RelationGetRelid(rel), fkconstraint->fk_attrs, fkattnum, fktypoid); + if (is_temporal) + { + /* + * First see if we have a period name, otherwise look for a range column. + * If it's a period, get the attnums of both start & end columns, + * but instead of their types get period's rangetype. + */ + transformPeriodName(RelationGetRelid(rel), + fkconstraint->fk_period, fkperiodattnums, fkperiodtypoids, &fkperiod); + + if (fkperiod == InvalidOid) + transformColumnNameList(RelationGetRelid(rel), + list_make1(fkconstraint->fk_period), + fkperiodattnums, fkperiodtypoids); + } /* * If the attribute list for the referenced table was omitted, lookup the @@ -9617,6 +9652,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid, &fkconstraint->pk_attrs, pkattnum, pktypoid, + &fkconstraint->pk_period, + &pkperiod, + pkperiodattnums, pkperiodtypoids, opclasses); } else @@ -9624,8 +9662,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numpks = transformColumnNameList(RelationGetRelid(pkrel), fkconstraint->pk_attrs, pkattnum, pktypoid); + if (is_temporal) { + transformPeriodName(RelationGetRelid(pkrel), + fkconstraint->pk_period, pkperiodattnums, pkperiodtypoids, &pkperiod); + + if (pkperiod == InvalidOid) + 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, + pkperiod, opclasses); } @@ -9684,187 +9733,43 @@ 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)) + 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) { + if (pkperiod == InvalidOid) { - pfeqop_right = fktyped; - ffeqop = get_opfamily_member(opfamily, fktyped, fktyped, - eqstrategy); + pkattnum[numpks] = pkperiodattnums[0]; + pktypoid[numpks] = pkperiodtypoids[0]; } else { - /* keep compiler quiet */ - pfeqop_right = InvalidOid; - ffeqop = InvalidOid; + pkattnum[numpks] = InvalidOid; + pktypoid[numpks] = 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) + if (fkperiod == InvalidOid) { - /* - * 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); + fkattnum[numpks] = fkperiodattnums[0]; + fktypoid[numpks] = fkperiodtypoids[0]; } - if (old_check_ok) + else { - 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)); + fkattnum[numpks] = InvalidOid; + fktypoid[numpks] = InvalidOid; } - 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; } /* @@ -9880,7 +9785,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, pfeqoperators, ppeqoperators, ffeqoperators, - old_check_ok); + old_check_ok, + is_temporal, + pkperiod, + fkperiod); /* Now handle the referencing side. */ addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel, @@ -9893,6 +9801,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, + pkperiod, + fkperiod, lockmode); /* @@ -9933,7 +9844,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, - Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok) + Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok, + bool is_temporal, Oid pkperiod, Oid fkperiod) { ObjectAddress address; Oid constrOid; @@ -10015,8 +9927,9 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ - false, /* conTemporal */ - InvalidOid, + is_temporal, + fkperiod, + pkperiod, false); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); @@ -10091,7 +10004,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, partIndexId, constrOid, numfks, mapped_pkattnum, fkattnum, pfeqoperators, ppeqoperators, ffeqoperators, - old_check_ok); + old_check_ok, is_temporal, pkperiod, fkperiod); /* Done -- clean up (but keep the lock) */ table_close(partRel, NoLock); @@ -10140,7 +10053,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok, LOCKMODE lockmode) + bool old_check_ok, bool is_temporal, Oid pkperiod, + Oid fkperiod, LOCKMODE lockmode) { AssertArg(OidIsValid(parentConstr)); @@ -10183,6 +10097,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); @@ -10285,8 +10200,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, false, 1, false, - false, /* conTemporal */ - InvalidOid, + is_temporal, + fkperiod, + pkperiod, false); /* @@ -10313,6 +10229,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, + pkperiod, + fkperiod, lockmode); table_close(partition, NoLock); @@ -10450,6 +10369,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop); + Assert(numfks == attmap->maplen); for (int i = 0; i < numfks; i++) mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; @@ -10496,7 +10416,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop, - true); + true, + constrForm->contemporal, + constrForm->confperiod, + constrForm->conperiod); table_close(fkRel, NoLock); ReleaseSysCache(tuple); @@ -10689,8 +10612,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) false, /* islocal */ 1, /* inhcount */ false, /* conNoInherit */ - false, /* conTemporal */ - InvalidOid, + constrForm->contemporal, + constrForm->conperiod, + constrForm->confperiod, true); /* Set up partition dependencies for the new constraint */ @@ -10720,11 +10644,232 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) conppeqop, conffeqop, false, /* no old check exists */ + constrForm->contemporal, + constrForm->conperiod, + constrForm->confperiod, AccessExclusiveLock); table_close(pkrel, NoLock); } } +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 @@ -11337,6 +11482,38 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, return address; } +/* + * transformPeriodName - transform period name to its base columns + * + * Lookup the name and return zero as the column attnum. Set + * the period's rangetype (not the columns' type OIDs). + */ +static void +transformPeriodName(Oid relId, Node *periodName, + int16 *attnums, Oid *atttypids, Oid *periodid) +{ + const char *periodStr = strVal(periodName); + HeapTuple pertuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(relId), + PointerGetDatum(periodStr)); + Form_pg_period period; + + if (!HeapTupleIsValid(pertuple)) + { + attnums[0] = InvalidOid; + atttypids[0] = InvalidOid; + *periodid = InvalidOid; + return; + } + + period = (Form_pg_period) GETSTRUCT(pertuple); + + attnums[0] = InvalidOid; + atttypids[0] = period->perrngtype; + *periodid = period->oid; + + ReleaseSysCache(pertuple); +} /* * transformColumnNameList - transform list of column names @@ -11362,6 +11539,7 @@ transformColumnNameList(Oid relId, List *colList, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" referenced in foreign key constraint does not exist", attname))); + if (attnum >= INDEX_MAX_KEYS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), @@ -11383,6 +11561,9 @@ transformColumnNameList(Oid relId, List *colList, * 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. + * For ranges we only set one thing for each of those. For periods we set + * the start/end column names and attnums, but the typids we set to the + * period's range type. * * All parameters except pkrel are output parameters. Also, the function * return value is the number of attributes in the primary key, @@ -11394,6 +11575,8 @@ static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, Oid *periodid, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses) { List *indexoidlist; @@ -11460,36 +11643,84 @@ 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) + { + if (pkattno == InvalidOid) + { + /* we have a period */ + HeapTuple periodTuple = SearchSysCache1(PERIODOID, ObjectIdGetDatum(indexStruct->indperiod)); + Form_pg_period period; + + if (!HeapTupleIsValid(periodTuple)) + elog(ERROR, "cache lookup failed for period %u", indexStruct->indperiod); + + period = (Form_pg_period) GETSTRUCT(periodTuple); + attnums[i] = InvalidOid; + periodattnums[0] = period->perstart; + periodattnums[1] = period->perend; + periodatttypids[0] = period->perrngtype; + periodatttypids[1] = period->perrngtype; + opclasses[i] = indclass->values[i]; + *periodid = period->oid; + *periodattname = (Node *)makeString(pstrdup(NameStr(period->pername))); + + ReleaseSysCache(periodTuple); + } + else { + /* we have a range */ + /* The caller will set attnums[i] */ + periodattnums[0] = pkattno; + periodatttypids[0] = attnumTypeId(pkrel, pkattno); + opclasses[i] = indclass->values[i]; + *periodid = InvalidOid; + *periodattname = (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))))); + *periodid = InvalidOid; + } } 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. + * It's okay to have a PERIOD referencing a range or a range referencing + * a PERIOD, as long as the types match up. If the PK uses a PERIOD, + * then periodid should be set and periodattnums should contain the + * start and end attnums. Otherwise periodattnums should contain the + * range attnum. + * 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 *periodattnums, + Oid periodid, Oid *opclasses) /* output parameter */ { Oid indexoid = InvalidOid; @@ -11499,6 +11730,7 @@ transformFkeyCheckAttrs(Relation pkrel, ListCell *indexoidscan; int i, j; + bool has_period = periodid != InvalidOid; /* * Reject duplicate appearances of columns in the referenced-columns list. @@ -11516,6 +11748,10 @@ transformFkeyCheckAttrs(Relation pkrel, (errcode(ERRCODE_INVALID_FOREIGN_KEY), errmsg("foreign key referenced-columns list must not contain duplicates"))); } + if (is_temporal && attnums[i] == periodattnums[0]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_FOREIGN_KEY), + errmsg("foreign key referenced-columns list must not contain duplicates"))); } /* @@ -11537,15 +11773,19 @@ 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 (unless + * it has a PERIOD). 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)) + (indexStruct->indperiod != InvalidOid || heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))) { Datum indclassDatum; bool isnull; @@ -11582,6 +11822,32 @@ transformFkeyCheckAttrs(Relation pkrel, if (!found) break; } + if (found && is_temporal) + { + if (has_period) + { + if (periodid == indexStruct->indperiod) + { + found = true; + opclasses[numattrs] = indclass->values[numattrs - 1]; + } + else + found = false; + } + else + { + found = false; + for (j = 0; j < numattrs + 1; j++) + { + if (periodattnums[0] == indexStruct->indkey.values[j]) + { + opclasses[numattrs] = indclass->values[j]; + found = true; + break; + } + } + } + } /* * Refuse to use a deferrable unique/primary key. This is per SQL @@ -11691,7 +11957,8 @@ validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, - Oid constraintOid) + Oid constraintOid, + bool temporal) { TupleTableSlot *slot; TableScanDesc scan; @@ -11721,8 +11988,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; /* @@ -11783,6 +12052,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, Oid constraintOid, Oid indexOid, bool on_insert) { CreateTrigStmt *fk_trigger; + bool is_temporal = fkconstraint->fk_period; /* * Note: for a self-referential FK (referencing and referenced tables are @@ -11796,18 +12066,27 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, fk_trigger = makeNode(CreateTrigStmt); fk_trigger->replace = false; fk_trigger->isconstraint = true; - fk_trigger->trigname = "RI_ConstraintTrigger_c"; + if (is_temporal) + fk_trigger->trigname = "TRI_ConstraintTrigger_c"; + else + fk_trigger->trigname = "RI_ConstraintTrigger_c"; fk_trigger->relation = NULL; /* 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; } @@ -11856,37 +12135,76 @@ 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: + 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: + 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", + (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; + } } (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel), @@ -11913,37 +12231,76 @@ 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: + 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: + 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", + (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; + } } (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel), diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index a503d638be..cd5653e317 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -839,6 +839,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, true, /* noinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ isInternal); /* is_internal */ } @@ -3885,6 +3886,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; } diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index ec48ac32c7..1bb400a482 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3554,6 +3554,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, false, /* connoinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ false); /* is_internal */ if (constrAddr) ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 833212fb7f..d34e590f84 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3118,7 +3118,9 @@ _copyConstraint(const Constraint *from) COPY_NODE_FIELD(where_clause); COPY_NODE_FIELD(pktable); COPY_NODE_FIELD(fk_attrs); + COPY_NODE_FIELD(fk_period); COPY_NODE_FIELD(pk_attrs); + COPY_NODE_FIELD(pk_period); COPY_SCALAR_FIELD(fk_matchtype); COPY_SCALAR_FIELD(fk_upd_action); COPY_SCALAR_FIELD(fk_del_action); @@ -3220,6 +3222,7 @@ _copyQuery(const Query *from) COPY_SCALAR_FIELD(canSetTag); COPY_NODE_FIELD(utilityStmt); COPY_SCALAR_FIELD(resultRelation); + COPY_NODE_FIELD(forPortionOf); COPY_SCALAR_FIELD(hasAggs); COPY_SCALAR_FIELD(hasWindowFuncs); COPY_SCALAR_FIELD(hasTargetSRFs); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d7ffb1d7bb..81a3cec904 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -499,11 +499,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type TableElement TypedTableElement ConstraintElem TableFuncElement %type columnDef columnOptions %type def_elem reloption_elem old_aggr_elem operator_def_elem -%type def_arg columnElem withoutOverlapsClause +%type def_arg columnElem withoutOverlapsClause 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 opt_column_and_period_list %type rowsfrom_item rowsfrom_list opt_col_def_list %type opt_ordinality %type ExclusionConstraintList ExclusionConstraintElem @@ -3941,19 +3942,21 @@ 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 = (char) ($10 >> 8); - n->fk_del_action = (char) ($10 & 0xFF); - 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 = (char) ($11 >> 8); + n->fk_del_action = (char) ($11 & 0xFF); + processCASbits($12, @12, "FOREIGN KEY", &n->deferrable, &n->initdeferred, &n->skip_validation, NULL, yyscanner); @@ -3981,6 +3984,16 @@ withoutOverlapsClause: | /*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); diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c index 5a38178485..12af464cea 100644 --- a/src/backend/utils/adt/rangetypes.c +++ b/src/backend/utils/adt/rangetypes.c @@ -2130,6 +2130,35 @@ make_empty_range(TypeCacheEntry *typcache) return make_range(typcache, &lower, &upper, true); } +/* + * Convert a range to a string. This isn't used anywhere but it handy for + * debugging. Perhaps we should remove it? + */ +char * +range_as_string(RangeType *r) +{ + int16 typlen; + bool typbyval; + char typalign; + char typdelim; + char *rangeStr; + Oid typioparam; + Oid range_out_oid; + + get_type_io_data( + RangeTypeGetOid(r), + IOFunc_output, + &typlen, + &typbyval, + &typalign, + &typdelim, + &typioparam, + &range_out_oid); + rangeStr = OidOutputFunctionCall(range_out_oid, RangeTypePGetDatum(r)); + + return rangeStr; +} + /* *---------------------------------------------------------- diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 96269fc2ad..c906725ec7 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -31,6 +31,8 @@ #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" +#include "catalog/pg_range.h" #include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -78,6 +80,10 @@ #define RI_PLAN_RESTRICT_CHECKREF 5 #define RI_PLAN_SETNULL_DOUPDATE 6 #define RI_PLAN_SETDEFAULT_DOUPDATE 7 +#define TRI_PLAN_CASCADE_DEL_DODELETE 8 +#define TRI_PLAN_CASCADE_UPD_DOUPDATE 9 +#define TRI_PLAN_SETNULL_DOUPDATE 10 +#define TRI_PLAN_SETDEFAULT_DOUPDATE 11 #define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3) #define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2) @@ -111,7 +117,18 @@ typedef struct RI_ConstraintInfo char confupdtype; /* foreign key's ON UPDATE action */ char confdeltype; /* foreign key's ON DELETE action */ char confmatchtype; /* foreign key's match type */ + bool temporal; /* if the foreign key is temporal */ int nkeys; /* number of key columns */ + Oid pk_period; /* set if the temporal primary key has a period */ + Oid fk_period; /* set if the temporal foreign key has a period */ + Oid pk_period_rangetype; /* set if the temporal primary key has a period */ + Oid fk_period_rangetype; /* set if the temporal foreign key has a period */ + char pk_period_rangetype_name[NAMEDATALEN]; /* pk period's rangetype's name */ + char fk_period_rangetype_name[NAMEDATALEN]; /* fk period's rangetype's name */ + Oid pk_period_collation; /* pk period's rangetype's collation */ + Oid fk_period_collation; /* fk period's rangetype's collation */ + int16 pk_period_attnums[2]; /* attnums of the referenced period cols */ + int16 fk_period_attnums[2]; /* attnums of the referencing period cols */ 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) */ @@ -181,6 +198,8 @@ 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); +static Datum tri_set(TriggerData *trigdata, bool is_set_null); +static Datum restrict_cascading_range(Datum pkRecordRange, Datum targetedRange); static void quoteOneName(char *buffer, const char *name); static void quoteRelationName(char *buffer, Relation rel); static void ri_GenerateQual(StringInfo buf, @@ -194,7 +213,7 @@ 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_AttributesEqual(Oid eq_opr, Oid typeid, Datum oldvalue, Datum newvalue); @@ -209,6 +228,7 @@ static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind); static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk); +static void DeconstructFkConstraintPeriod(Oid periodid, Oid *rangetypeid, char *rangetypename, Oid *rngcollation, int16 *attnums); static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); static Oid get_ri_constraint_root(Oid constrOid); static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, @@ -217,16 +237,99 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, RI_QueryKey *qkey, SPIPlanPtr qplan, Relation fk_rel, Relation pk_rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, + bool hasForPortionOf, Datum forPortionOf, bool detectNewRows, int expect_OK); static void ri_ExtractValues(Relation rel, TupleTableSlot *slot, const RI_ConstraintInfo *riinfo, bool rel_is_pk, + bool hasForPortionOf, Datum forPortionOf, Datum *vals, char *nulls); 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 Datum tupleRange(TupleTableSlot *slot, const RI_ConstraintInfo *riinfo); +/* + * query_period_range + * + * Builds a SQL string to construct a range from a period's start and end columns. + */ +static char * +query_period_range(const RI_ConstraintInfo *riinfo, Relation rel, const char *table_name, bool rel_is_pk) +{ + StringInfo str = makeStringInfo(); + char attname[MAX_QUOTED_NAME_LEN + 3]; + int table_name_len = strlen(table_name); + + Assert(table_name_len <= 3); + + strcpy(attname, table_name); + if (rel_is_pk) + { + appendStringInfo(str, "%s(", riinfo->pk_period_rangetype_name); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->pk_period_attnums[0])); + appendStringInfo(str, "%s, ", attname); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->pk_period_attnums[1])); + appendStringInfo(str, "%s)", attname); + } + else + { + appendStringInfo(str, "%s(", riinfo->fk_period_rangetype_name); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->fk_period_attnums[0])); + appendStringInfo(str, "%s, ", attname); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->fk_period_attnums[1])); + appendStringInfo(str, "%s)", attname); + } + + return pstrdup(str->data); +} + + +static Datum +build_period_range(const RI_ConstraintInfo *riinfo, TupleTableSlot *slot, bool rel_is_pk) +{ + const int16 *period_attnums = rel_is_pk ? riinfo->pk_period_attnums : riinfo->fk_period_attnums; + Oid rangetype = rel_is_pk ? riinfo->pk_period_rangetype : riinfo->fk_period_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, period_attnums[0], &startisnull); + endvalue = slot_getattr(slot, period_attnums[1], &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; +} + /* * RI_FKey_check - * @@ -348,6 +451,7 @@ RI_FKey_check(TriggerData *trigdata) StringInfoData querybuf; char pkrelname[MAX_QUOTED_REL_NAME_LEN]; char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; char paramname[16]; const char *querysep; Oid queryoids[RI_MAX_NUMKEYS]; @@ -355,35 +459,86 @@ RI_FKey_check(TriggerData *trigdata) /* ---------- * The query string built is - * SELECT 1 FROM [ONLY] x WHERE pkatt1 = $1 [AND ...] - * FOR KEY SHARE OF x + * SELECT 1 + * FROM [ONLY] 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) + { + if (riinfo->pk_period == InvalidOid) + { + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1])); + attstr = attname; + } + else + attstr = query_period_range(riinfo, pk_rel, "x.", true); + + appendStringInfo(&querybuf, + "SELECT 1 FROM (SELECT %s AS r FROM %s%s x", + attstr, 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; + + if (riinfo->pk_attnums[i] == InvalidOid) + { + pk_type = riinfo->pk_period_rangetype; + attstr = query_period_range(riinfo, pk_rel, "x.", true); + } + else + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + attstr = attname; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + fk_type = riinfo->fk_period_rangetype; + else + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); - quoteOneName(attname, - RIAttName(pk_rel, riinfo->pk_attnums[i])); sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, - attname, pk_type, + attstr, pk_type, riinfo->pf_eq_oprs[i], paramname, fk_type); querysep = "AND"; 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, @@ -400,6 +555,7 @@ RI_FKey_check(TriggerData *trigdata) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, NULL, newslot, + false, 0, pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE, SPI_OK_SELECT); @@ -502,13 +658,24 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, querysep = "WHERE"; for (int i = 0; i < riinfo->nkeys; i++) { - Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + Oid pk_type; + char *attstr; + if (riinfo->pk_attnums[i] == InvalidOid) + { + pk_type = riinfo->pk_period_rangetype; + attstr = query_period_range(riinfo, pk_rel, "x.", true); + } + else + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + attstr = attname; + } - quoteOneName(attname, - RIAttName(pk_rel, riinfo->pk_attnums[i])); sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, - attname, pk_type, + attstr, pk_type, riinfo->pp_eq_oprs[i], paramname, pk_type); querysep = "AND"; @@ -527,6 +694,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, result = ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* treat like update */ SPI_OK_SELECT); @@ -667,6 +835,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) StringInfoData querybuf; char fkrelname[MAX_QUOTED_REL_NAME_LEN]; char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; char paramname[16]; const char *querysep; Oid queryoids[RI_MAX_NUMKEYS]; @@ -689,18 +858,46 @@ 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; - quoteOneName(attname, - RIAttName(fk_rel, riinfo->fk_attnums[i])); + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "x.", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, paramname, pk_type, riinfo->pf_eq_oprs[i], - attname, fk_type); + attstr, fk_type); if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) ri_GenerateQualCollation(&querybuf, pk_coll); querysep = "AND"; @@ -719,6 +916,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* must detect new rows */ SPI_OK_SELECT); @@ -825,6 +1023,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* must detect new rows */ SPI_OK_DELETE); @@ -946,6 +1145,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, newslot, + false, 0, true, /* must detect new rows */ SPI_OK_UPDATE); @@ -1095,17 +1295,888 @@ ri_set(TriggerData *trigdata, bool is_set_null) 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])); - appendStringInfo(&querybuf, - "%s %s = %s", - querysep, attname, - is_set_null ? "NULL" : "DEFAULT"); + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + appendStringInfo(&querybuf, + "%s %s = %s", + querysep, attname, + is_set_null ? "NULL" : "DEFAULT"); + sprintf(paramname, "$%d", i + 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; + } + appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len); + + /* Prepare and save the plan */ + qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, 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, + false, 0, + 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 - + * + * Check if we really need to fire the RI trigger for an update or delete to a PK + * relation. This is called by the AFTER trigger queue manager to see if + * it can skip queuing an instance of an RI trigger. Returns true if the + * trigger must be fired, false if we can prove the constraint will still + * be satisfied. + * + * newslot will be NULL if this is called for a delete. + */ +bool +RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, + TupleTableSlot *oldslot, TupleTableSlot *newslot) +{ + const RI_ConstraintInfo *riinfo; + + riinfo = ri_FetchConstraintInfo(trigger, pk_rel, true); + + /* + * If any old key value is NULL, the row could not have been referenced by + * an FK row, so no check is needed. + */ + if (ri_NullCheck(RelationGetDescr(pk_rel), oldslot, riinfo, true) != RI_KEYS_NONE_NULL) + return false; + + /* If all old and new key values are equal, no check is needed */ + if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true)) + return false; + + /* Else we need to fire the trigger. */ + return true; +} + +/* + * RI_FKey_fk_upd_check_required - + * + * Check if we really need to fire the RI trigger for an update to an FK + * relation. This is called by the AFTER trigger queue manager to see if + * it can skip queuing an instance of an RI trigger. Returns true if the + * trigger must be fired, false if we can prove the constraint will still + * be satisfied. + */ +bool +RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, + TupleTableSlot *oldslot, TupleTableSlot *newslot) +{ + const RI_ConstraintInfo *riinfo; + int ri_nullcheck; + Datum xminDatum; + TransactionId xmin; + bool isnull; + + riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false); + + ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false); + + /* + * If all new key values are NULL, the row satisfies the constraint, so no + * check is needed. + */ + if (ri_nullcheck == RI_KEYS_ALL_NULL) + return false; + + /* + * If some new key values are NULL, the behavior depends on the match + * type. + */ + else if (ri_nullcheck == RI_KEYS_SOME_NULL) + { + switch (riinfo->confmatchtype) + { + case FKCONSTR_MATCH_SIMPLE: + + /* + * If any new key value is NULL, the row must satisfy the + * constraint, so no check is needed. + */ + return false; + + case FKCONSTR_MATCH_PARTIAL: + + /* + * Don't know, must run full check. + */ + break; + + case FKCONSTR_MATCH_FULL: + + /* + * If some new key values are NULL, the row fails the + * constraint. We must not throw error here, because the row + * might get invalidated before the constraint is to be + * checked, but we should queue the event to apply the check + * later. + */ + return true; + } + } + + /* + * Continues here for no new key values are NULL, or we couldn't decide + * yet. + */ + + /* + * If the original row was inserted by our own transaction, we must fire + * the trigger whether or not the keys are equal. This is because our + * UPDATE will invalidate the INSERT so that the INSERT RI trigger will + * not do anything; so we had better do the UPDATE check. (We could skip + * this if we knew the INSERT trigger already fired, but there is no easy + * way to know that.) + */ + xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull); + Assert(!isnull); + xmin = DatumGetTransactionId(xminDatum); + if (TransactionIdIsCurrentTransactionId(xmin)) + return true; + + /* If all old and new key values are equal, no check is needed */ + if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false)) + return false; + + /* Else we need to fire the trigger. */ + return true; +} + +/* ---------- + * 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_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; + + /* + * If this is a FOR PORTION OF delete, + * restrict the casacde to just the targeted portion. + */ + targetRange = tupleRange(oldslot, riinfo); + if (trigdata->tg_temporal) + targetRange = restrict_cascading_range(targetRange, + trigdata->tg_temporal->fp_targetRange); + + 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_DEL_DODELETE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * DELETE FROM [ONLY] + * 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); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + 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; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + sprintf(paramname, "$%d", i + 1); + ri_GenerateQual(&querybuf, querysep, + paramname, pk_type, + riinfo->pf_eq_oprs[i], + attstr, fk_type); + if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) + ri_GenerateQualCollation(&querybuf, pk_coll); + querysep = "AND"; + queryoids[i] = pk_type; + } + + if (pkHasRange) + queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[riinfo->nkeys] = riinfo->pk_period_rangetype; + + /* 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, + true, 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 - + * + * 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); +} + +/* + * 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; + + /* + * If this is a FOR PORTION OF update, + * restrict the casacde to just the targeted portion. + */ + targetRange = tupleRange(oldslot, riinfo); + if (trigdata->tg_temporal) + targetRange = restrict_cascading_range(targetRange, + trigdata->tg_temporal->fp_targetRange); + + 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_UPD_DOUPDATE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + StringInfoData qualbuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + const char *qualsep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * UPDATE [ONLY] + * FOR PORTION OF $fkatt FROM lower($n+1) TO upper($n+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); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + 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; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + /* + * 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, attstr, i + 1); + sprintf(paramname, "$%d", j + 1); + ri_GenerateQual(&qualbuf, qualsep, + paramname, pk_type, + riinfo->pf_eq_oprs[i], + attstr, 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); + + if (pkHasRange) + queryoids[2 * riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[2 * riinfo->nkeys] = riinfo->pk_period_rangetype; + + /* 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. Build up the arguments from the key values in the + * updated PK tuple and update the referencing rows + */ + ri_PerformCheck(riinfo, &qkey, qplan, + fk_rel, pk_rel, + oldslot, newslot, + true, 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); +} + +/* + * 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); +} + +/* + * 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); +} + +/* + * 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); +} + + +/* + * restrict_cascading_range - + * + * When we cascade an UPDATE or DELETE on a temporal record, + * we only touch the temporal portion of the child record + * that matches the temporal span of the updated/deleted parent record. + * But if the original UPDATE or DELETE also had a FOR PORTION OF clause, + * then we should further restrict the cascaded effect accordingly. + */ +static Datum +restrict_cascading_range(Datum pkRecordRange, Datum targetedRange) +{ + RangeType *r1 = DatumGetRangeTypeP(pkRecordRange); + RangeType *r2 = DatumGetRangeTypeP(targetedRange); + Oid rngtypid = RangeTypeGetOid(r1); + TypeCacheEntry *typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO); + return RangeTypePGetDatum(range_intersect_internal(typcache, r1, r2)); +} + +/* + * 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) +{ + const RI_ConstraintInfo *riinfo; + Relation fk_rel; + Relation pk_rel; + TupleTableSlot *oldslot; + RI_QueryKey qkey; + SPIPlanPtr qplan; + Datum targetRange; + + 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; + + /* + * If this is a FOR PORTION OF update/delete, + * restrict the casacde to just the targeted portion. + */ + targetRange = tupleRange(oldslot, riinfo); + if (trigdata->tg_temporal) + targetRange = restrict_cascading_range(targetRange, + trigdata->tg_temporal->fp_targetRange); + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* + * Fetch or prepare a saved plan for the set null/default operation (it's + * the same query for delete and update cases) + */ + ri_BuildQueryKey(&qkey, riinfo, + is_set_null + ? TRI_PLAN_SETNULL_DOUPDATE + : TRI_PLAN_SETDEFAULT_DOUPDATE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + StringInfoData qualbuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + const char *qualsep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * UPDATE [ONLY] + * 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); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + 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); + + querysep = ""; + qualsep = "WHERE"; + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + + /* + * Don't set the temporal column(s). + * FOR PORTION OF will take care of that. + */ + if (i < riinfo->nkeys - 1) + appendStringInfo(&querybuf, + "%s %s = %s", + querysep, attstr, + is_set_null ? "NULL" : "DEFAULT"); sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&qualbuf, qualsep, paramname, pk_type, riinfo->pf_eq_oprs[i], - attname, fk_type); + attstr, fk_type); if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) ri_GenerateQualCollation(&querybuf, pk_coll); querysep = ","; @@ -1114,8 +2185,13 @@ ri_set(TriggerData *trigdata, bool is_set_null) } appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len); + if (pkHasRange) + queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[riinfo->nkeys] = riinfo->pk_period_rangetype; + /* Prepare and save the plan */ - qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids, + qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids, &qkey, fk_rel, pk_rel); } @@ -1125,6 +2201,7 @@ ri_set(TriggerData *trigdata, bool is_set_null) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + true, targetRange, true, /* must detect new rows */ SPI_OK_UPDATE); @@ -1155,132 +2232,6 @@ ri_set(TriggerData *trigdata, bool is_set_null) } -/* - * RI_FKey_pk_upd_check_required - - * - * Check if we really need to fire the RI trigger for an update or delete to a PK - * relation. This is called by the AFTER trigger queue manager to see if - * it can skip queuing an instance of an RI trigger. Returns true if the - * trigger must be fired, false if we can prove the constraint will still - * be satisfied. - * - * newslot will be NULL if this is called for a delete. - */ -bool -RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) -{ - const RI_ConstraintInfo *riinfo; - - riinfo = ri_FetchConstraintInfo(trigger, pk_rel, true); - - /* - * If any old key value is NULL, the row could not have been referenced by - * an FK row, so no check is needed. - */ - if (ri_NullCheck(RelationGetDescr(pk_rel), oldslot, riinfo, true) != RI_KEYS_NONE_NULL) - 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)) - return false; - - /* Else we need to fire the trigger. */ - return true; -} - -/* - * RI_FKey_fk_upd_check_required - - * - * Check if we really need to fire the RI trigger for an update to an FK - * relation. This is called by the AFTER trigger queue manager to see if - * it can skip queuing an instance of an RI trigger. Returns true if the - * trigger must be fired, false if we can prove the constraint will still - * be satisfied. - */ -bool -RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) -{ - const RI_ConstraintInfo *riinfo; - int ri_nullcheck; - Datum xminDatum; - TransactionId xmin; - bool isnull; - - riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false); - - ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false); - - /* - * If all new key values are NULL, the row satisfies the constraint, so no - * check is needed. - */ - if (ri_nullcheck == RI_KEYS_ALL_NULL) - return false; - - /* - * If some new key values are NULL, the behavior depends on the match - * type. - */ - else if (ri_nullcheck == RI_KEYS_SOME_NULL) - { - switch (riinfo->confmatchtype) - { - case FKCONSTR_MATCH_SIMPLE: - - /* - * If any new key value is NULL, the row must satisfy the - * constraint, so no check is needed. - */ - return false; - - case FKCONSTR_MATCH_PARTIAL: - - /* - * Don't know, must run full check. - */ - break; - - case FKCONSTR_MATCH_FULL: - - /* - * If some new key values are NULL, the row fails the - * constraint. We must not throw error here, because the row - * might get invalidated before the constraint is to be - * checked, but we should queue the event to apply the check - * later. - */ - return true; - } - } - - /* - * Continues here for no new key values are NULL, or we couldn't decide - * yet. - */ - - /* - * If the original row was inserted by our own transaction, we must fire - * the trigger whether or not the keys are equal. This is because our - * UPDATE will invalidate the INSERT so that the INSERT RI trigger will - * not do anything; so we had better do the UPDATE check. (We could skip - * this if we knew the INSERT trigger already fired, but there is no easy - * way to know that.) - */ - xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull); - Assert(!isnull); - xmin = DatumGetTransactionId(xminDatum); - if (TransactionIdIsCurrentTransactionId(xmin)) - return true; - - /* If all old and new key values are equal, no check is needed */ - if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false)) - return false; - - /* Else we need to fire the trigger. */ - return true; -} /* * RI_Initial_Check - @@ -1306,6 +2257,8 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) StringInfoData querybuf; char pkrelname[MAX_QUOTED_REL_NAME_LEN]; char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char *pkattpart; + char *fkattpart; char pkattname[MAX_QUOTED_NAME_LEN + 3]; char fkattname[MAX_QUOTED_NAME_LEN + 3]; RangeTblEntry *pkrte; @@ -1387,10 +2340,24 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) sep = ""; for (int i = 0; i < riinfo->nkeys; i++) { - quoteOneName(fkattname, - RIAttName(fk_rel, riinfo->fk_attnums[i])); - appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); - sep = ", "; + if (riinfo->fk_attnums[i] != InvalidOid) + { + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + sep = ", "; + } + else + { + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_period_attnums[0])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + sep = ", "; + + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_period_attnums[1])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + } } quoteRelationName(pkrelname, pk_rel); @@ -1408,19 +2375,47 @@ 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; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + + quoteOneName(pkattname + 3, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + pkattpart = pkattname; + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + pkattpart = query_period_range(riinfo, pk_rel, "pk.", true); + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + + quoteOneName(fkattname + 3, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + fkattpart = fkattname; + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + fkattpart = query_period_range(riinfo, pk_rel, "fk.", false); + } - 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, + pkattpart, pk_type, riinfo->pf_eq_oprs[i], - fkattname, fk_type); + fkattpart, fk_type); if (pk_coll != fk_coll) ri_GenerateQualCollation(&querybuf, pk_coll); sep = "AND"; @@ -2034,6 +3029,43 @@ ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk) return riinfo; } +static void DeconstructFkConstraintPeriod(Oid periodid, Oid *rangetypeid, char *rangetypename, Oid *rngcollation, int16 *attnums) +{ + HeapTuple pertuple = SearchSysCache1(PERIODOID, periodid); + Form_pg_period period; + Form_pg_range rangetype; + Form_pg_type type; + HeapTuple rngtuple; + HeapTuple typtuple; + + if (!HeapTupleIsValid(pertuple)) + elog(ERROR, "cache lookup failed for period %d", periodid); + + period = (Form_pg_period) GETSTRUCT(pertuple); + + *rangetypeid = period->perrngtype; + attnums[0] = period->perstart; + attnums[1] = period->perend; + ReleaseSysCache(pertuple); + + rngtuple = SearchSysCache1(RANGETYPE, *rangetypeid); + if (!HeapTupleIsValid(rngtuple)) + elog(ERROR, "cache lookup failed for range %d", *rangetypeid); + + rangetype = (Form_pg_range) GETSTRUCT(rngtuple); + + *rngcollation = rangetype->rngcollation; + ReleaseSysCache(rngtuple); + + typtuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(*rangetypeid)); + if (!HeapTupleIsValid(typtuple)) + elog(ERROR, "cache lookup failed for type %u", *rangetypeid); + + type = (Form_pg_type) GETSTRUCT(typtuple); + strcpy(rangetypename, NameStr(type->typname)); + ReleaseSysCache(typtuple); +} + /* * Fetch or create the RI_ConstraintInfo struct for an FK constraint. */ @@ -2057,6 +3089,7 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache, (void *) &constraintOid, HASH_ENTER, &found); + if (!found) riinfo->valid = false; else if (riinfo->valid) @@ -2091,6 +3124,24 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo->confupdtype = conForm->confupdtype; riinfo->confdeltype = conForm->confdeltype; riinfo->confmatchtype = conForm->confmatchtype; + riinfo->temporal = conForm->contemporal; + riinfo->pk_period = conForm->confperiod; + riinfo->pk_period_rangetype = InvalidOid; + riinfo->pk_period_collation = InvalidOid; + strcpy(riinfo->pk_period_rangetype_name, ""); + memset(riinfo->pk_period_attnums, 0, sizeof(riinfo->pk_period_attnums)); + riinfo->fk_period = conForm->conperiod; + riinfo->fk_period_rangetype = InvalidOid; + riinfo->fk_period_collation = InvalidOid; + strcpy(riinfo->fk_period_rangetype_name, ""); + memset(riinfo->fk_period_attnums, 0, sizeof(riinfo->fk_period_attnums)); + + if (conForm->confperiod != InvalidOid) + { + DeconstructFkConstraintPeriod(conForm->confperiod, &riinfo->pk_period_rangetype, riinfo->pk_period_rangetype_name, &riinfo->pk_period_collation, riinfo->pk_period_attnums); + } + if (conForm->conperiod != InvalidOid) + DeconstructFkConstraintPeriod(conForm->conperiod, &riinfo->fk_period_rangetype, riinfo->fk_period_rangetype_name, &riinfo->fk_period_collation, riinfo->fk_period_attnums); DeconstructFkConstraintRow(tup, &riinfo->nkeys, @@ -2242,6 +3293,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, RI_QueryKey *qkey, SPIPlanPtr qplan, Relation fk_rel, Relation pk_rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, + bool hasForPortionOf, Datum forPortionOf, bool detectNewRows, int expect_OK) { Relation query_rel, @@ -2287,15 +3339,16 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, if (newslot) { ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk, - vals, nulls); + hasForPortionOf, forPortionOf, vals, nulls); if (oldslot) ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk, + hasForPortionOf, forPortionOf, vals + riinfo->nkeys, nulls + riinfo->nkeys); } else { ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk, - vals, nulls); + hasForPortionOf, forPortionOf, vals, nulls); } /* @@ -2377,6 +3430,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, static void ri_ExtractValues(Relation rel, TupleTableSlot *slot, const RI_ConstraintInfo *riinfo, bool rel_is_pk, + bool hasForPortionOf, Datum forPortionOf, Datum *vals, char *nulls) { const int16 *attnums; @@ -2389,8 +3443,22 @@ ri_ExtractValues(Relation rel, TupleTableSlot *slot, for (int i = 0; i < riinfo->nkeys; i++) { - vals[i] = slot_getattr(slot, attnums[i], &isnull); - nulls[i] = isnull ? 'n' : ' '; + if (attnums[i] != InvalidOid) + { + vals[i] = slot_getattr(slot, attnums[i], &isnull); + nulls[i] = isnull ? 'n' : ' '; + } + else + { + vals[i] = build_period_range(riinfo, slot, rel_is_pk); + nulls[i] = ' '; + } + } + + if (hasForPortionOf) + { + vals[riinfo->nkeys] = forPortionOf; + nulls[riinfo->nkeys] = ' '; } } @@ -2413,6 +3481,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, StringInfoData key_values; bool onfk; const int16 *attnums; + const int16 *period_attnums; Oid rel_oid; AclResult aclresult; bool has_perm = true; @@ -2425,6 +3494,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, if (onfk) { attnums = riinfo->fk_attnums; + period_attnums = riinfo->fk_period_attnums; rel_oid = fk_rel->rd_id; if (tupdesc == NULL) tupdesc = fk_rel->rd_att; @@ -2432,6 +3502,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, else { attnums = riinfo->pk_attnums; + period_attnums = riinfo->pk_period_attnums; rel_oid = pk_rel->rd_id; if (tupdesc == NULL) tupdesc = pk_rel->rd_att; @@ -2484,12 +3555,18 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, for (int idx = 0; idx < riinfo->nkeys; idx++) { int fnum = attnums[idx]; - Form_pg_attribute att = TupleDescAttr(tupdesc, fnum - 1); + bool has_period = fnum == InvalidOid; char *name, *val; + Form_pg_attribute att; Datum datum; bool isnull; + if (has_period) + fnum = period_attnums[0]; + + att = TupleDescAttr(tupdesc, fnum - 1); + name = NameStr(att->attname); datum = slot_getattr(violatorslot, fnum, &isnull); @@ -2511,6 +3588,32 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, } appendStringInfoString(&key_names, name); appendStringInfoString(&key_values, val); + + if (has_period) + { + fnum = period_attnums[1]; + + att = TupleDescAttr(tupdesc, fnum - 1); + + name = NameStr(att->attname); + + datum = slot_getattr(violatorslot, fnum, &isnull); + if (!isnull) + { + Oid foutoid; + bool typisvarlena; + + getTypeOutputInfo(att->atttypid, &foutoid, &typisvarlena); + val = OidOutputFunctionCall(foutoid, datum); + } + else + val = "null"; + + appendStringInfoString(&key_names, ", "); + appendStringInfoString(&key_values, ", "); + appendStringInfoString(&key_names, name); + appendStringInfoString(&key_values, val); + } } } @@ -2577,10 +3680,21 @@ ri_NullCheck(TupleDesc tupDesc, for (int i = 0; i < riinfo->nkeys; i++) { - if (slot_attisnull(slot, attnums[i])) - nonenull = false; - else + if (attnums[i] == InvalidOid) + { + /* + * Never treat a period as null, because even if start and end + * are both null, that just signifies an unbounded range. + */ allnull = false; + } + else + { + if (slot_attisnull(slot, attnums[i])) + nonenull = false; + else + allnull = false; + } } if (allnull) @@ -2711,9 +3825,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 @@ -2721,7 +3838,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; @@ -2738,19 +3855,32 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, Datum newvalue; bool isnull; - /* - * Get one attribute's oldvalue. If it is NULL - they're not equal. - */ - oldvalue = slot_getattr(oldslot, attnums[i], &isnull); - if (isnull) - return false; + if (riinfo->temporal && attnums[i] == InvalidAttrNumber) + { + /* + * We have a period, so we have to get the start/end columns + * and build a range. + */ + oldvalue = build_period_range(riinfo, oldslot, rel_is_pk); + newvalue = build_period_range(riinfo, newslot, rel_is_pk); + } + else + { + /* + * Get one attribute's oldvalue. If it is NULL - they're not equal. + */ + oldvalue = slot_getattr(oldslot, attnums[i], &isnull); + if (isnull) + return false; - /* - * Get one attribute's newvalue. If it is NULL - they're not equal. - */ - newvalue = slot_getattr(newslot, attnums[i], &isnull); - if (isnull) - return false; + /* + * Get one attribute's newvalue. If it is NULL - they're not equal. + */ + newvalue = slot_getattr(newslot, attnums[i], &isnull); + if (isnull) + return false; + + } if (rel_is_pk) { @@ -2950,3 +4080,17 @@ RI_FKey_trigger_type(Oid tgfoid) return RI_TRIGGER_NONE; } + +static Datum +tupleRange(TupleTableSlot *slot, const RI_ConstraintInfo *riinfo) +{ + bool isnull; + int16 attnum = riinfo->pk_attnums[riinfo->nkeys - 1]; + + if (attnum == InvalidOid) + /* Build a range from the PERIOD start and end columns */ + return build_period_range(riinfo, slot, true); + else + /* Get the range from the range column */ + return slot_getattr(slot, attnum, &isnull); +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index bb59ffc616..1c773bcb64 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -333,7 +333,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, Oid periodid, StringInfo buf); + bool withoutOverlaps, bool withPeriod, Oid periodid, 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, @@ -2196,7 +2196,12 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null conkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, InvalidOid, &buf); + /* + * If it is a temporal foreign key + * then it uses PERIOD (which may be a real range column + * or may be a period). + */ + decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, conForm->contemporal, conForm->conperiod, &buf); /* add foreign relation name */ appendStringInfo(&buf, ") REFERENCES %s(", @@ -2210,7 +2215,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null confkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, InvalidOid, &buf); + decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, conForm->contemporal, conForm->confperiod, &buf); appendStringInfoChar(&buf, ')'); @@ -2318,7 +2323,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, InvalidOid, &buf); + keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, false, InvalidOid, &buf); appendStringInfoChar(&buf, ')'); @@ -2518,7 +2523,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, Oid periodid, StringInfo buf) + bool withoutOverlaps, bool withPeriod, Oid periodid, StringInfo buf) { Datum *keys; int nKeys; @@ -2567,6 +2572,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 6c6675b0f7..2babe42713 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -114,6 +114,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) bool contemporal; Oid conperiod; /* local PERIOD used in PK/FK constraint */ + Oid confperiod; /* referenced foreign PERIOD */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ @@ -129,19 +130,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); @@ -176,7 +177,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, on /* 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 @@ -237,6 +238,7 @@ extern Oid CreateConstraintEntry(const char *constraintName, bool conNoInherit, bool conTemporal, Oid period, + Oid fperiod, bool is_internal); extern void RemoveConstraintById(Oid conId); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index c3963acd4b..268975a4ca 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3887,6 +3887,50 @@ 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 => '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 => '', + 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 => '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 => '', + 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' }, + # Temporal leftovers triggers { oid => '8157', descr => 'temporal leftovers trigger function', proname => 'FP_insert_leftovers', provolatile => 'v', diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d2a59ca755..588f6407ae 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2338,7 +2338,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/utils/rangetypes.h b/src/include/utils/rangetypes.h index d8c1f1656a..fa18218bb2 100644 --- a/src/include/utils/rangetypes.h +++ b/src/include/utils/rangetypes.h @@ -150,5 +150,6 @@ extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2, RangeType **output1, RangeType **output2); +extern char *range_as_string(RangeType *r); #endif /* RANGETYPES_H */ diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index ac814814ef..5d9f2c1081 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -179,6 +179,7 @@ quad_poly_tbl|t radix_text_tbl|t ramp|f real_city|f +referencing_period_test|t reservations|f road|t shighway|t diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out index 708b5d3528..05fd62f4c3 100644 --- a/src/test/regress/expected/without_overlaps.out +++ b/src/test/regress/expected/without_overlaps.out @@ -284,3 +284,1849 @@ ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING ts ALTER TABLE without_overlaps_test2 RENAME COLUMN valid_at TO valid_thru; ALTER TABLE without_overlaps_test2 DROP COLUMN valid_thru; DROP TABLE without_overlaps_test2; +-- +-- test FK dependencies +-- +-- can't drop a range referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at; +ERROR: cannot drop column valid_at of table without_overlaps_test2 because other objects depend on it +DETAIL: constraint referencing_period2_fk on table referencing_period_test2 depends on column valid_at of table without_overlaps_test2 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to constraint referencing_period2_fk on table referencing_period_test2 +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; +-- can't drop a PERIOD referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at; +ERROR: cannot drop period valid_at on table without_overlaps_test2 because other objects depend on it +DETAIL: constraint referencing_period2_fk on table referencing_period_test2 depends on period valid_at on table without_overlaps_test2 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at CASCADE; +NOTICE: drop cascades to constraint referencing_period2_fk on table referencing_period_test2 +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; +-- +-- test FOREIGN KEY, range references range +-- +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +DROP TABLE referencing_period_test; +-- with inferred PK on the referenced table: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test 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 "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test 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 "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test 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 without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +----+----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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) + +-- +-- test FOREIGN KEY, range references PERIOD +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_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 "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +----+----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test 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 without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test 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) + +-- +-- test FOREIGN KEY, PERIOD references range +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO without_overlaps_test VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES ('[3,3]', tsrange('2018-01-01', NULL)); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Tue May 01 00:00:00 2018) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([8,9), Tue Jan 02 00:00:00 2018, Thu Mar 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test 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 "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test 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 "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +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 "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test 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 without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +----+------------+-----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_from | valid_til | 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) + +-- +-- test FOREIGN KEY, PERIOD references PERIOD +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Tue May 01 00:00:00 2018) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([8,9), Tue Jan 02 00:00:00 2018, Thu Mar 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +----+------------+-----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | 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 without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | 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 without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | 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 without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_from | valid_til | 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) + diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql index e64ec69f0b..5ade1b8079 100644 --- a/src/test/regress/sql/without_overlaps.sql +++ b/src/test/regress/sql/without_overlaps.sql @@ -293,3 +293,1405 @@ ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING ts ALTER TABLE without_overlaps_test2 RENAME COLUMN valid_at TO valid_thru; ALTER TABLE without_overlaps_test2 DROP COLUMN valid_thru; DROP TABLE without_overlaps_test2; + +-- +-- test FK dependencies +-- + +-- can't drop a range referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at; +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at CASCADE; +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; + +-- can't drop a PERIOD referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at; +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at CASCADE; +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; + +-- +-- test FOREIGN KEY, range references range +-- + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +DROP TABLE referencing_period_test; + +-- with inferred PK on the referenced table: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); + +-- +-- test FK child updates +-- + +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +UPDATE referencing_period_test 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 without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +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 referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +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 referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test 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 referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test 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 referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test 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 without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + + +-- +-- test FOREIGN KEY, range references PERIOD +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +UPDATE referencing_period_test 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 without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + +-- +-- test FOREIGN KEY, PERIOD references range +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO without_overlaps_test VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES ('[3,3]', tsrange('2018-01-01', NULL)); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +UPDATE referencing_period_test 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 without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +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 referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test 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 without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test 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 without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +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 referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test 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 referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test 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 referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test 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 without_overlaps_test 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 referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test 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 without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + +-- +-- test FOREIGN KEY, PERIOD references PERIOD +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +UPDATE referencing_period_test 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 without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; -- 2.32.0