diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 4a2b6f0dae..1a50a7f235 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -78,9 +78,9 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ CONSTRAINT constraint_name ]
{ CHECK ( expression ) [ NO INHERIT ] |
UNIQUE ( column_name [, ... ] ) index_parameters |
- PRIMARY KEY ( column_name [, ... ] ) 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 ]
@@ -105,6 +105,11 @@ WITH ( MODULUS numeric_literal, REM
exclude_element in an EXCLUDE constraint is:
{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]
+
+temporal_interval in a PRIMARY KEY or FOREIGN KEY constraint is:
+
+range_column_name
+
@@ -909,7 +914,8 @@ WITH ( MODULUS numeric_literal, REM
PRIMARY KEY (column constraint)
- PRIMARY KEY ( column_name [, ... ] )
+ PRIMARY KEY ( column_name [, ... ]
+ [, temporal_interval WITHOUT OVERLAPS ] )
INCLUDE ( column_name [, ...]) (table constraint)
@@ -942,13 +948,31 @@ WITH ( MODULUS numeric_literal, REM
Adding a PRIMARY KEY constraint will automatically
- create a unique btree index on the column or group of columns used in the
- constraint. The optional INCLUDE clause allows a list
- of columns to be specified which will be included in the non-key portion
- of the index. Although uniqueness is not enforced on the included columns,
- the constraint still depends on them. Consequently, some operations on the
- included columns (e.g. DROP COLUMN) can cause cascaded
- constraint and index deletion.
+ create a unique btree (or GiST if temporal) index on the column or group of
+ columns used in the constraint. The optional INCLUDE clause
+ allows a list of columns to be specified which will be included in the non-key
+ portion of the index. Although uniqueness is not enforced on the included
+ columns, the constraint still depends on them. Consequently, some operations
+ on the included columns (e.g. DROP COLUMN) can cause
+ cascaded constraint and index deletion.
+
+
+
+ A PRIMARY KEY with a WITHOUT OVERLAPS option
+ is a temporal primary key.
+ The WITHOUT OVERLAPS column
+ must be a range type and is used to constrain the record's applicability
+ to just that range (usually a range of dates or timestamps).
+ The main part of the primary key may be repeated elsewhere in the table,
+ as long as records with the same key don't overlap in the
+ WITHOUT OVERLAPS column.
+
+
+
+ A temporal PRIMARY KEY is enforced with an
+ EXCLUDE constraint rather than a UNIQUE
+ constraint, backed by a GiST index. You may need to install the
+ extension to create temporal primary keys.
@@ -1006,8 +1030,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 ]
@@ -1018,11 +1042,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 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
diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm
index dd39a086ce..47afc4ef7b 100644
--- a/src/backend/catalog/Catalog.pm
+++ b/src/backend/catalog/Catalog.pm
@@ -237,6 +237,7 @@ sub ParseData
# Scan the input file.
while (<$ifd>)
{
+ next if /^#/;
my $hash_ref;
if (/{/)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 9d9e915979..1d0a26e146 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2430,6 +2430,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
is_local, /* conislocal */
inhcount, /* coninhcount */
is_no_inherit, /* connoinherit */
+ false, /* contemporal */
is_internal); /* internally constructed? */
pfree(ccbin);
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7223679033..a6ddbcbfdf 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1787,6 +1787,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId)
* INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row
* INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies
* of index on table's columns
+ * INDEX_CONSTR_CREATE_TEMPORAL: constraint is for a temporal primary key
* allow_system_table_mods: allow table to be a system catalog
* is_internal: index is constructed due to internal process
*/
@@ -1810,11 +1811,13 @@ index_constraint_create(Relation heapRelation,
bool mark_as_primary;
bool islocal;
bool noinherit;
+ bool is_temporal;
int inhcount;
deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0;
initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0;
mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0;
+ is_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
/* constraint creation support doesn't work while bootstrapping */
Assert(!IsBootstrapProcessingMode());
@@ -1889,6 +1892,7 @@ index_constraint_create(Relation heapRelation,
islocal,
inhcount,
noinherit,
+ is_temporal, /* contemporal */
is_internal);
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 90932be831..f08b68d800 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -75,6 +75,7 @@ CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
bool is_internal)
{
Relation conDesc;
@@ -183,6 +184,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal);
values[Anum_pg_constraint_coninhcount - 1] = Int32GetDatum(conInhCount);
values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
+ values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal);
if (conkeyArray)
values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 3f3a89fe92..973de18201 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1001,6 +1001,8 @@ DefineIndex(Oid relationId,
constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
if (stmt->initdeferred)
constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+ if (stmt->istemporal)
+ constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
indexRelationId =
index_create(rel, indexRelationName, indexRelationId, parentIndexId,
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7a13b97164..779e89827b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -333,9 +333,12 @@ static int transformColumnNameList(Oid relId, List *colList,
static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ Node **periodattname,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses);
static Oid transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 *periodattnums,
Oid *opclasses);
static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts);
static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
@@ -343,7 +346,7 @@ static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId,
static void validateCheckConstraint(Relation rel, HeapTuple constrtup);
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);
@@ -446,12 +449,12 @@ 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);
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, LOCKMODE lockmode);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -468,6 +471,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,
@@ -4973,7 +4982,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
validateForeignKeyConstraint(fkconstraint->conname, rel, refrel,
con->refindid,
- con->conid);
+ con->conid,
+ fkconstraint->fk_period != NULL);
/*
* No need to mark the constraint row as validated, we did
@@ -8095,6 +8105,11 @@ 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 pkperiodattnum = 0;
+ int16 fkperiodattnum = 0;
+ Oid pkperiodtypoid = 0;
+ Oid fkperiodtypoid = 0;
int i;
int numfks,
numpks;
@@ -8197,6 +8212,14 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfks = transformColumnNameList(RelationGetRelid(rel),
fkconstraint->fk_attrs,
fkattnum, fktypoid);
+ if (is_temporal)
+ {
+ List *fk_period;
+ fk_period = list_make1(fkconstraint->fk_period);
+ transformColumnNameList(RelationGetRelid(rel),
+ fk_period,
+ &fkperiodattnum, &fkperiodtypoid);
+ }
/*
* If the attribute list for the referenced table was omitted, lookup the
@@ -8209,6 +8232,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
&fkconstraint->pk_attrs,
pkattnum, pktypoid,
+ &fkconstraint->pk_period,
+ &pkperiodattnum, &pkperiodtypoid,
opclasses);
}
else
@@ -8216,8 +8241,15 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformColumnNameList(RelationGetRelid(pkrel),
fkconstraint->pk_attrs,
pkattnum, pktypoid);
+ if (is_temporal) {
+ List *pk_period = list_make1(fkconstraint->pk_period);
+ transformColumnNameList(RelationGetRelid(pkrel),
+ pk_period,
+ &pkperiodattnum, &pkperiodtypoid);
+ }
/* Look for an index matching the column list */
indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum,
+ is_temporal, &pkperiodattnum,
opclasses);
}
@@ -8267,6 +8299,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
(errcode(ERRCODE_INVALID_FOREIGN_KEY),
errmsg("number of referencing and referenced columns for foreign key disagree")));
+ // TODO: Need a check that if one side has a PERIOD the other does too
+
/*
* On the strength of a previous constraint, we might avoid scanning
* tables to validate this one. See below.
@@ -8276,187 +8310,27 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
for (i = 0; i < numpks; i++)
{
- Oid pktype = pktypoid[i];
- Oid fktype = fktypoid[i];
- Oid fktyped;
- HeapTuple cla_ht;
- Form_pg_opclass cla_tup;
- Oid amid;
- Oid opfamily;
- Oid opcintype;
- Oid pfeqop;
- Oid ppeqop;
- Oid ffeqop;
- int16 eqstrategy;
- Oid pfeqop_right;
-
- /* We need several fields out of the pg_opclass entry */
- cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i]));
- if (!HeapTupleIsValid(cla_ht))
- elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]);
- cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht);
- amid = cla_tup->opcmethod;
- opfamily = cla_tup->opcfamily;
- opcintype = cla_tup->opcintype;
- ReleaseSysCache(cla_ht);
-
- /*
- * Check it's a btree; currently this can never fail since no other
- * index AMs support unique indexes. If we ever did have other types
- * of unique indexes, we'd need a way to determine which operator
- * strategy number is equality. (Is it reasonable to insist that
- * every such index AM use btree's number for equality?)
- */
- if (amid != BTREE_AM_OID)
- elog(ERROR, "only b-tree indexes are supported for foreign keys");
- eqstrategy = BTEqualStrategyNumber;
-
- /*
- * There had better be a primary equality operator for the index.
- * We'll use it for PK = PK comparisons.
- */
- ppeqop = get_opfamily_member(opfamily, opcintype, opcintype,
- eqstrategy);
-
- if (!OidIsValid(ppeqop))
- elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
- eqstrategy, opcintype, opcintype, opfamily);
-
- /*
- * Are there equality operators that take exactly the FK type? Assume
- * we should look through any domain here.
- */
- fktyped = getBaseType(fktype);
-
- pfeqop = get_opfamily_member(opfamily, opcintype, fktyped,
- eqstrategy);
- if (OidIsValid(pfeqop))
- {
- pfeqop_right = fktyped;
- ffeqop = get_opfamily_member(opfamily, fktyped, fktyped,
- eqstrategy);
- }
- else
- {
- /* keep compiler quiet */
- pfeqop_right = InvalidOid;
- ffeqop = InvalidOid;
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- {
- /*
- * Otherwise, look for an implicit cast from the FK type to the
- * opcintype, and if found, use the primary equality operator.
- * This is a bit tricky because opcintype might be a polymorphic
- * type such as ANYARRAY or ANYENUM; so what we have to test is
- * whether the two actual column types can be concurrently cast to
- * that type. (Otherwise, we'd fail to reject combinations such
- * as int[] and point[].)
- */
- Oid input_typeids[2];
- Oid target_typeids[2];
-
- input_typeids[0] = pktype;
- input_typeids[1] = fktype;
- target_typeids[0] = opcintype;
- target_typeids[1] = opcintype;
- if (can_coerce_type(2, input_typeids, target_typeids,
- COERCION_IMPLICIT))
- {
- pfeqop = ffeqop = ppeqop;
- pfeqop_right = opcintype;
- }
- }
-
- if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop)))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("foreign key constraint \"%s\" cannot be implemented",
- fkconstraint->conname),
- errdetail("Key columns \"%s\" and \"%s\" "
- "are of incompatible types: %s and %s.",
- strVal(list_nth(fkconstraint->fk_attrs, i)),
- strVal(list_nth(fkconstraint->pk_attrs, i)),
- format_type_be(fktype),
- format_type_be(pktype))));
-
- if (old_check_ok)
- {
- /*
- * When a pfeqop changes, revalidate the constraint. We could
- * permit intra-opfamily changes, but that adds subtle complexity
- * without any concrete benefit for core types. We need not
- * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
- */
- old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item));
- old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
- old_pfeqop_item);
- }
- if (old_check_ok)
- {
- Oid old_fktype;
- Oid new_fktype;
- CoercionPathType old_pathtype;
- CoercionPathType new_pathtype;
- Oid old_castfunc;
- Oid new_castfunc;
- Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
- fkattnum[i] - 1);
-
- /*
- * Identify coercion pathways from each of the old and new FK-side
- * column types to the right (foreign) operand type of the pfeqop.
- * We may assume that pg_constraint.conkey is not changing.
- */
- old_fktype = attr->atttypid;
- new_fktype = fktype;
- old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
- &old_castfunc);
- new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
- &new_castfunc);
-
- /*
- * Upon a change to the cast from the FK column to its pfeqop
- * operand, revalidate the constraint. For this evaluation, a
- * binary coercion cast is equivalent to no cast at all. While
- * type implementors should design implicit casts with an eye
- * toward consistency of operations like equality, we cannot
- * assume here that they have done so.
- *
- * A function with a polymorphic argument could change behavior
- * arbitrarily in response to get_fn_expr_argtype(). Therefore,
- * when the cast destination is polymorphic, we only avoid
- * revalidation if the input type has not changed at all. Given
- * just the core data types and operator classes, this requirement
- * prevents no would-be optimizations.
- *
- * If the cast converts from a base type to a domain thereon, then
- * that domain type must be the opcintype of the unique index.
- * Necessarily, the primary key column must then be of the domain
- * type. Since the constraint was previously valid, all values on
- * the foreign side necessarily exist on the primary side and in
- * turn conform to the domain. Consequently, we need not treat
- * domains specially here.
- *
- * Since we require that all collations share the same notion of
- * equality (which they do, because texteq reduces to bitwise
- * equality), we don't compare collation here.
- *
- * We need not directly consider the PK type. It's necessarily
- * binary coercible to the opcintype of the unique index column,
- * and ri_triggers.c will only deal with PK datums in terms of
- * that opcintype. Changing the opcintype also changes pfeqop.
- */
- old_check_ok = (new_pathtype == old_pathtype &&
- new_castfunc == old_castfunc &&
- (!IsPolymorphicType(pfeqop_right) ||
- new_fktype == old_fktype));
- }
+ FindFKComparisonOperators(
+ fkconstraint, tab, i, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pktypoid[i], fktypoid[i], opclasses[i],
+ is_temporal, false,
+ &pfeqoperators[i], &ppeqoperators[i], &ffeqoperators[i]);
+ }
+ if (is_temporal) {
+ pkattnum[numpks] = pkperiodattnum;
+ fkattnum[numpks] = fkperiodattnum;
+ pktypoid[numpks] = pkperiodtypoid;
+ fktypoid[numpks] = fkperiodtypoid;
- pfeqoperators[i] = pfeqop;
- ppeqoperators[i] = ppeqop;
- ffeqoperators[i] = ffeqop;
+ FindFKComparisonOperators(
+ fkconstraint, tab, numpks, fkattnum,
+ &old_check_ok, &old_pfeqop_item,
+ pkperiodtypoid, fkperiodtypoid, opclasses[numpks],
+ is_temporal, true,
+ &pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]);
+ numfks += 1;
+ numpks += 1;
}
/*
@@ -8472,7 +8346,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ is_temporal);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8485,6 +8360,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
+ is_temporal,
lockmode);
/*
@@ -8525,7 +8401,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)
{
ObjectAddress address;
Oid constrOid;
@@ -8607,6 +8484,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
+ is_temporal,
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -8681,7 +8559,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);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -8730,7 +8608,7 @@ 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, LOCKMODE lockmode)
{
AssertArg(OidIsValid(parentConstr));
@@ -8875,6 +8753,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
+ is_temporal,
false);
/*
@@ -8901,6 +8780,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
+ is_temporal,
lockmode);
table_close(partition, NoLock);
@@ -9039,6 +8919,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conppeqop,
conffeqop);
Assert(numfks == attmap->maplen);
+
for (int i = 0; i < numfks; i++)
mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
@@ -9084,7 +8965,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ constrForm->contemporal);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
@@ -9277,6 +9159,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
+ constrForm->contemporal,
true);
/* Set up partition dependencies for the new constraint */
@@ -9306,11 +9189,214 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
+ constrForm->contemporal,
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)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("foreign key constraint \"%s\" cannot be implemented",
+ fkconstraint->conname),
+ errdetail("Key columns \"%s\" and \"%s\" "
+ "are of incompatible types: %s and %s.",
+ strVal(list_nth(fkconstraint->fk_attrs, i)),
+ strVal(list_nth(fkconstraint->pk_attrs, i)),
+ format_type_be(fktype),
+ format_type_be(pktype))));
+
+ if (*old_check_ok)
+ {
+ /*
+ * When a pfeqop changes, revalidate the constraint. We could
+ * permit intra-opfamily changes, but that adds subtle complexity
+ * without any concrete benefit for core types. We need not
+ * assess ppeqop or ffeqop, which RI_Initial_Check() does not use.
+ */
+ *old_check_ok = (pfeqop == lfirst_oid(*old_pfeqop_item));
+ *old_pfeqop_item = lnext(fkconstraint->old_conpfeqop,
+ *old_pfeqop_item);
+ }
+ if (*old_check_ok)
+ {
+ Oid old_fktype;
+ Oid new_fktype;
+ CoercionPathType old_pathtype;
+ CoercionPathType new_pathtype;
+ Oid old_castfunc;
+ Oid new_castfunc;
+ Form_pg_attribute attr = TupleDescAttr(tab->oldDesc,
+ fkattnum[i] - 1);
+
+ /*
+ * Identify coercion pathways from each of the old and new FK-side
+ * column types to the right (foreign) operand type of the pfeqop.
+ * We may assume that pg_constraint.conkey is not changing.
+ */
+ old_fktype = attr->atttypid;
+ new_fktype = fktype;
+ old_pathtype = findFkeyCast(pfeqop_right, old_fktype,
+ &old_castfunc);
+ new_pathtype = findFkeyCast(pfeqop_right, new_fktype,
+ &new_castfunc);
+
+ /*
+ * Upon a change to the cast from the FK column to its pfeqop
+ * operand, revalidate the constraint. For this evaluation, a
+ * binary coercion cast is equivalent to no cast at all. While
+ * type implementors should design implicit casts with an eye
+ * toward consistency of operations like equality, we cannot
+ * assume here that they have done so.
+ *
+ * A function with a polymorphic argument could change behavior
+ * arbitrarily in response to get_fn_expr_argtype(). Therefore,
+ * when the cast destination is polymorphic, we only avoid
+ * revalidation if the input type has not changed at all. Given
+ * just the core data types and operator classes, this requirement
+ * prevents no would-be optimizations.
+ *
+ * If the cast converts from a base type to a domain thereon, then
+ * that domain type must be the opcintype of the unique index.
+ * Necessarily, the primary key column must then be of the domain
+ * type. Since the constraint was previously valid, all values on
+ * the foreign side necessarily exist on the primary side and in
+ * turn conform to the domain. Consequently, we need not treat
+ * domains specially here.
+ *
+ * Since we require that all collations share the same notion of
+ * equality (which they do, because texteq reduces to bitwise
+ * equality), we don't compare collation here.
+ *
+ * We need not directly consider the PK type. It's necessarily
+ * binary coercible to the opcintype of the unique index column,
+ * and ri_triggers.c will only deal with PK datums in terms of
+ * that opcintype. Changing the opcintype also changes pfeqop.
+ */
+ *old_check_ok = (new_pathtype == old_pathtype &&
+ new_castfunc == old_castfunc &&
+ (!IsPolymorphicType(pfeqop_right) ||
+ new_fktype == old_fktype));
+
+ }
+
+ *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
@@ -9691,7 +9777,7 @@ ATExecValidateConstraint(Relation rel, char *constrName, bool recurse,
validateForeignKeyConstraint(constrName, rel, refrel,
con->conindid,
- con->oid);
+ con->oid, con->contemporal);
table_close(refrel, NoLock);
/*
@@ -9824,10 +9910,12 @@ transformColumnNameList(Oid relId, List *colList,
*
* Look up the names, attnums, and types of the primary key attributes
* for the pkrel. Also return the index OID and index opclasses of the
- * index supporting the primary key.
+ * index supporting the primary key. If this is a temporal primary key,
+ * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
*
* All parameters except pkrel are output parameters. Also, the function
- * return value is the number of attributes in the primary key.
+ * return value is the number of attributes in the primary key,
+ * not including the WITHOUT OVERLAPS if any.
*
* Used when the column list in the REFERENCES specification is omitted.
*/
@@ -9835,6 +9923,8 @@ static int
transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ Node **periodattname,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses)
{
List *indexoidlist;
@@ -9902,35 +9992,50 @@ 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)
+ * TODO: range expressions will be how we support PERIODs though.
*/
*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)
+ {
+ periodattnums[0] = pkattno;
+ periodatttypids[0] = attnumTypeId(pkrel, pkattno);
+ opclasses[i] = indclass->values[i];
+ *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)))));
+ }
}
ReleaseSysCache(indexTuple);
- return i;
+ if (indexStruct->indisexclusion) return i - 1;
+ else return i;
}
/*
* transformFkeyCheckAttrs -
*
* Make sure that the attributes of a referenced table belong to a unique
- * (or primary key) constraint. Return the OID of the index supporting
- * the constraint, as well as the opclasses associated with the index
+ * (or primary key) constraint. Or if this is a temporal foreign key
+ * the primary key should be an exclusion constraint instead.
+ * Return the OID of the index supporting the constraint,
+ * as well as the opclasses associated with the index
* columns.
*/
static Oid
transformFkeyCheckAttrs(Relation pkrel,
int numattrs, int16 *attnums,
+ bool is_temporal, int16 *periodattnums,
Oid *opclasses) /* output parameter */
{
Oid indexoid = InvalidOid;
@@ -9957,6 +10062,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")));
}
/*
@@ -9978,12 +10087,16 @@ transformFkeyCheckAttrs(Relation pkrel,
indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
/*
- * Must have the right number of columns; must be unique and not a
+ * Must have the right number of columns; must be unique
+ * (or if temporal then exclusion instead) and not a
* partial index; forget it if there are any expressions, too. Invalid
* indexes are out as well.
*/
- if (indexStruct->indnkeyatts == numattrs &&
- indexStruct->indisunique &&
+ 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))
@@ -10023,6 +10136,19 @@ transformFkeyCheckAttrs(Relation pkrel,
if (!found)
break;
}
+ if (is_temporal)
+ {
+ 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
@@ -10213,7 +10339,8 @@ validateForeignKeyConstraint(char *conname,
Relation rel,
Relation pkrel,
Oid pkindOid,
- Oid constraintOid)
+ Oid constraintOid,
+ bool temporal)
{
TupleTableSlot *slot;
TableScanDesc scan;
@@ -10243,8 +10370,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;
/*
@@ -10306,6 +10435,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
@@ -10317,7 +10447,10 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* and "RI_ConstraintTrigger_c_NNNN" for the check triggers.
*/
fk_trigger = makeNode(CreateTrigStmt);
- 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;
fk_trigger->row = true;
fk_trigger->timing = TRIGGER_TYPE_AFTER;
@@ -10325,12 +10458,18 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
/* Either ON INSERT or ON UPDATE */
if (on_insert)
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_ins");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins");
fk_trigger->events = TRIGGER_TYPE_INSERT;
}
else
{
- fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
+ if (is_temporal)
+ fk_trigger->funcname = SystemFuncName("TRI_FKey_check_upd");
+ else
+ fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd");
fk_trigger->events = TRIGGER_TYPE_UPDATE;
}
@@ -10376,37 +10515,78 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->isconstraint = true;
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;
+ }
}
fk_trigger->args = NIL;
@@ -10432,37 +10612,78 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
fk_trigger->whenClause = NULL;
fk_trigger->isconstraint = true;
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;
+ }
}
fk_trigger->args = NIL;
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index b408efb11e..be31226725 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -735,6 +735,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
true, /* islocal */
0, /* inhcount */
true, /* noinherit */
+ false, /* contemporal */
isInternal); /* is_internal */
}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 455786128b..a6b74c9f51 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3413,6 +3413,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
true, /* is local */
0, /* inhcount */
false, /* connoinherit */
+ false, /* contemporal */
false); /* is_internal */
if (constrAddr)
ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 28130fbc2b..7d04844f3f 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1322,6 +1322,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_projectReturning = NULL;
resultRelInfo->ri_onConflictArbiterIndexes = NIL;
resultRelInfo->ri_onConflict = NULL;
+ resultRelInfo->ri_forPortionOf = NULL;
resultRelInfo->ri_ReturningSlot = NULL;
resultRelInfo->ri_TrigOldSlot = NULL;
resultRelInfo->ri_TrigNewSlot = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d71c0a4322..35c137a397 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -55,6 +55,7 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/memutils.h"
+#include "utils/rangetypes.h"
#include "utils/rel.h"
@@ -614,8 +615,10 @@ ExecInsert(ModifyTableState *mtstate,
/* insert index entries for tuple */
if (resultRelInfo->ri_NumIndices > 0)
+ {
recheckIndexes = ExecInsertIndexTuples(slot, estate, false, NULL,
NIL);
+ }
}
}
@@ -676,6 +679,119 @@ ExecInsert(ModifyTableState *mtstate,
return result;
}
+/*
+ * Insert tuples for the untouched timespan of a row in a FOR PORTION OF UPDATE/DELETE
+ */
+static void
+ExecForPortionOfLeftovers(ModifyTableState *mtstate, EState *estate, ResultRelInfo *resultRelInfo, ItemPointer tupleid, TupleTableSlot *slot, TupleTableSlot *planSlot)
+{
+ // TODO: figure out if I need to make a copy of slot somehow in order to insert it....
+
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
+ Datum oldRange;
+ Datum targetRange;
+ RangeType *oldRangeType;
+ RangeType *targetRangeType;
+ RangeType *leftoverRangeType1;
+ RangeType *leftoverRangeType2;
+ Oid rangeTypeOid;
+ bool isNull;
+ TypeCacheEntry *typcache;
+ TupleTableSlot *oldtupleSlot = resultRelInfo->ri_forPortionOf->fp_Existing;
+ TupleTableSlot *leftoverTuple1 = resultRelInfo->ri_forPortionOf->fp_Leftover1;
+ TupleTableSlot *leftoverTuple2 = resultRelInfo->ri_forPortionOf->fp_Leftover2;
+ char *rangestr;
+
+ /* Get the range of the existing pre-UPDATE/DELETE tuple */
+
+ // TODO: Seems like we shouldn't have to do this,
+ // because the old tuple should already be available somehow?
+ // But this is what triggers do.... (Are you sure this is how they get the OLD tuple?)
+ // And even if we do have to do this, is SnapshotAny really correct?
+ // Shouldn't it be the snapshot of the UPDATE?
+ if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny,
+ oldtupleSlot))
+ elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
+
+ oldRange = slot_getattr(oldtupleSlot, forPortionOf->range_attno, &isNull);
+ if (isNull)
+ elog(ERROR, "found a NULL range in a temporal table");
+ oldRangeType = DatumGetRangeTypeP(oldRange);
+
+ /* Evaluate the target range if we haven't yet */
+
+ if (resultRelInfo->ri_forPortionOf->fp_targetRange)
+ targetRangeType = resultRelInfo->ri_forPortionOf->fp_targetRange;
+ else
+ {
+ ExprContext *econtext = GetPerTupleExprContext(estate);
+ econtext->ecxt_scantuple = slot;
+
+ ExprState *exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate);
+ targetRange = ExecEvalExpr(exprState, econtext, &isNull);
+
+ if (isNull) elog(ERROR, "Got a NULL FOR PORTION OF target range");
+ targetRangeType = DatumGetRangeTypeP(targetRange);
+ resultRelInfo->ri_forPortionOf->fp_targetRange = targetRangeType;
+ }
+
+
+ /*
+ * Get the range's type cache entry. This is worth caching for the whole UPDATE
+ * like range functions do.
+ */
+
+ typcache = resultRelInfo->ri_forPortionOf->fp_rangetypcache;
+ if (typcache == NULL)
+ {
+ rangeTypeOid = RangeTypeGetOid(oldRangeType);
+ typcache = lookup_type_cache(rangeTypeOid, TYPECACHE_RANGE_INFO);
+ if (typcache->rngelemtype == NULL)
+ elog(ERROR, "type %u is not a range type", rangeTypeOid);
+ resultRelInfo->ri_forPortionOf->fp_rangetypcache = typcache;
+ }
+
+ /* Get the ranges to the left/right of the targeted range. */
+
+ range_leftover_internal(typcache, oldRangeType, targetRangeType, &leftoverRangeType1,
+ &leftoverRangeType2);
+
+ /* Insert a copy of the tuple with the lower leftover range */
+
+ if (!RangeIsEmpty(leftoverRangeType1))
+ {
+ MinimalTuple oldtuple = ExecFetchSlotMinimalTuple(oldtupleSlot, NULL);
+ ExecForceStoreMinimalTuple(oldtuple, leftoverTuple1, false);
+
+ leftoverTuple1->tts_values[forPortionOf->range_attno - 1] = RangeTypePGetDatum(leftoverRangeType1);
+ leftoverTuple1->tts_isnull[forPortionOf->range_attno - 1] = false;
+
+ ExecMaterializeSlot(leftoverTuple1);
+
+ // TODO: tuple routing?
+ ExecInsert(mtstate, leftoverTuple1, planSlot,
+ estate, node->canSetTag);
+ }
+
+ /* Insert a copy of the tuple with the upper leftover range */
+
+ if (!RangeIsEmpty(leftoverRangeType2))
+ {
+ MinimalTuple oldtuple = ExecFetchSlotMinimalTuple(oldtupleSlot, NULL);
+ ExecForceStoreMinimalTuple(oldtuple, leftoverTuple2, false);
+
+ leftoverTuple2->tts_values[forPortionOf->range_attno - 1] = RangeTypePGetDatum(leftoverRangeType2);
+ leftoverTuple2->tts_isnull[forPortionOf->range_attno - 1] = false;
+
+ ExecMaterializeSlot(leftoverTuple2);
+
+ // TODO: tuple routing?
+ ExecInsert(mtstate, leftoverTuple2, planSlot,
+ estate, node->canSetTag);
+ }
+}
+
/* ----------------------------------------------------------------
* ExecDelete
*
@@ -717,6 +833,8 @@ ExecDelete(ModifyTableState *mtstate,
TM_FailureData tmfd;
TupleTableSlot *slot = NULL;
TransitionCaptureState *ar_delete_trig_tcs;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
if (tupleDeleted)
*tupleDeleted = false;
@@ -987,6 +1105,13 @@ ldelete:;
ar_delete_trig_tcs = NULL;
}
+ /*
+ * Compute leftovers in FOR PORTION OF
+ */
+ // TODO: Skip this for FDW deletes?
+ if (forPortionOf)
+ ExecForPortionOfLeftovers(mtstate, estate, resultRelInfo, tupleid, slot, planSlot);
+
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
@@ -1074,6 +1199,8 @@ ExecUpdate(ModifyTableState *mtstate,
TM_FailureData tmfd;
List *recheckIndexes = NIL;
TupleConversionMap *saved_tcs_map = NULL;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
/*
* abort the operation if not running transactions
@@ -1098,6 +1225,8 @@ ExecUpdate(ModifyTableState *mtstate,
return NULL; /* "do nothing" */
}
+ // TODO: Is there an argument that we should set the temporal bounds
+ // before calling the INSTEAD OF trigger?? What do other dbs do?
/* INSTEAD OF ROW UPDATE Triggers */
if (resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_instead_row)
@@ -1475,6 +1604,13 @@ lreplace:;
if (canSetTag)
(estate->es_processed)++;
+ /*
+ * Compute leftovers in FOR PORTION OF
+ */
+ // TODO: Skip this for FDW updates?
+ if (forPortionOf)
+ ExecForPortionOfLeftovers(mtstate, estate, resultRelInfo, tupleid, slot, planSlot);
+
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
recheckIndexes,
@@ -2367,7 +2503,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
* query.
*/
if (resultRelInfo->ri_RelationDesc->rd_rel->relhasindex &&
- operation != CMD_DELETE &&
+ (operation != CMD_DELETE || node->forPortionOf != NULL) &&
resultRelInfo->ri_IndexRelationDescs == NULL)
ExecOpenIndices(resultRelInfo,
node->onConflictAction != ONCONFLICT_NONE);
@@ -2589,6 +2725,31 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
/*
+ * If needed, initialize ... TODO ... for FOR PORTION OF.
+ */
+ if (node->forPortionOf)
+ {
+ // TODO: Is tupDesc the right thing?
+ TupleDesc tupDesc = resultRelInfo->ri_RelationDesc->rd_att;
+
+ /* create state for FOR PORTION OF operation */
+ resultRelInfo->ri_forPortionOf = makeNode(ForPortionOfState);
+
+ /* initialize slot for the existing tuple */
+ resultRelInfo->ri_forPortionOf->fp_Existing =
+ table_slot_create(resultRelInfo->ri_RelationDesc,
+ &mtstate->ps.state->es_tupleTable);
+
+ /* Create the tuple slots for INSERTing the leftovers. */
+ resultRelInfo->ri_forPortionOf->fp_Leftover1 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
+ &TTSOpsVirtual);
+ resultRelInfo->ri_forPortionOf->fp_Leftover2 =
+ ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
+ &TTSOpsVirtual);
+ }
+
+ /*
* If we have any secondary relations in an UPDATE or DELETE, they need to
* be treated like non-locked relations in SELECT FOR UPDATE, ie, the
* EvalPlanQual mechanism needs to be told about them. Locate the
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index eaab97f753..efcc1c26ff 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -217,6 +217,7 @@ _copyModifyTable(const ModifyTable *from)
COPY_NODE_FIELD(rowMarks);
COPY_SCALAR_FIELD(epqParam);
COPY_SCALAR_FIELD(onConflictAction);
+ COPY_NODE_FIELD(forPortionOf);
COPY_NODE_FIELD(arbiterIndexes);
COPY_NODE_FIELD(onConflictSet);
COPY_NODE_FIELD(onConflictWhere);
@@ -2209,6 +2210,28 @@ _copyOnConflictExpr(const OnConflictExpr *from)
return newnode;
}
+/*
+ * _copyForPortionOfExpr
+ */
+static ForPortionOfExpr *
+_copyForPortionOfExpr(const ForPortionOfExpr *from)
+{
+ ForPortionOfExpr *newnode = makeNode(ForPortionOfExpr);
+
+ COPY_SCALAR_FIELD(range_attno);
+ COPY_STRING_FIELD(range_name);
+ COPY_NODE_FIELD(range);
+ COPY_NODE_FIELD(startCol);
+ COPY_NODE_FIELD(endCol);
+ COPY_NODE_FIELD(targetStart);
+ COPY_NODE_FIELD(targetEnd);
+ COPY_NODE_FIELD(targetRange);
+ COPY_NODE_FIELD(overlapsExpr);
+ COPY_NODE_FIELD(rangeSet);
+
+ return newnode;
+}
+
/* ****************************************************************
* pathnodes.h copy functions
*
@@ -2549,6 +2572,19 @@ _copyOnConflictClause(const OnConflictClause *from)
return newnode;
}
+static ForPortionOfClause *
+_copyForPortionOfClause(const ForPortionOfClause *from)
+{
+ ForPortionOfClause *newnode = makeNode(ForPortionOfClause);
+
+ COPY_STRING_FIELD(range_name);
+ COPY_SCALAR_FIELD(range_name_location);
+ COPY_NODE_FIELD(target_start);
+ COPY_NODE_FIELD(target_end);
+
+ return newnode;
+}
+
static CommonTableExpr *
_copyCommonTableExpr(const CommonTableExpr *from)
{
@@ -2934,12 +2970,15 @@ _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);
COPY_NODE_FIELD(old_conpfeqop);
COPY_SCALAR_FIELD(old_pktable_oid);
+ COPY_NODE_FIELD(without_overlaps);
COPY_SCALAR_FIELD(skip_validation);
COPY_SCALAR_FIELD(initially_valid);
@@ -3481,6 +3520,7 @@ _copyIndexStmt(const IndexStmt *from)
COPY_SCALAR_FIELD(unique);
COPY_SCALAR_FIELD(primary);
COPY_SCALAR_FIELD(isconstraint);
+ COPY_SCALAR_FIELD(istemporal);
COPY_SCALAR_FIELD(deferrable);
COPY_SCALAR_FIELD(initdeferred);
COPY_SCALAR_FIELD(transformed);
@@ -5097,6 +5137,9 @@ copyObjectImpl(const void *from)
case T_OnConflictExpr:
retval = _copyOnConflictExpr(from);
break;
+ case T_ForPortionOfExpr:
+ retval = _copyForPortionOfExpr(from);
+ break;
/*
* RELATION NODES
@@ -5625,6 +5668,9 @@ copyObjectImpl(const void *from)
case T_OnConflictClause:
retval = _copyOnConflictClause(from);
break;
+ case T_ForPortionOfClause:
+ retval = _copyForPortionOfClause(from);
+ break;
case T_CommonTableExpr:
retval = _copyCommonTableExpr(from);
break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 88b912977e..a2be1dfeb4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -818,6 +818,23 @@ _equalOnConflictExpr(const OnConflictExpr *a, const OnConflictExpr *b)
return true;
}
+static bool
+_equalForPortionOfExpr(const ForPortionOfExpr *a, const ForPortionOfExpr *b)
+{
+ COMPARE_SCALAR_FIELD(range_attno);
+ COMPARE_STRING_FIELD(range_name);
+ COMPARE_NODE_FIELD(range);
+ COMPARE_NODE_FIELD(startCol);
+ COMPARE_NODE_FIELD(endCol);
+ COMPARE_NODE_FIELD(targetStart);
+ COMPARE_NODE_FIELD(targetEnd);
+ COMPARE_NODE_FIELD(targetRange);
+ COMPARE_NODE_FIELD(overlapsExpr);
+ COMPARE_NODE_FIELD(rangeSet);
+
+ return true;
+}
+
/*
* Stuff from pathnodes.h
*/
@@ -2826,6 +2843,17 @@ _equalOnConflictClause(const OnConflictClause *a, const OnConflictClause *b)
}
static bool
+_equalForPortionOfClause(const ForPortionOfClause *a, const ForPortionOfClause *b)
+{
+ COMPARE_STRING_FIELD(range_name);
+ COMPARE_SCALAR_FIELD(range_name_location);
+ COMPARE_NODE_FIELD(target_start);
+ COMPARE_NODE_FIELD(target_end);
+
+ return true;
+}
+
+static bool
_equalCommonTableExpr(const CommonTableExpr *a, const CommonTableExpr *b)
{
COMPARE_STRING_FIELD(ctename);
@@ -3201,6 +3229,9 @@ equal(const void *a, const void *b)
case T_OnConflictExpr:
retval = _equalOnConflictExpr(a, b);
break;
+ case T_ForPortionOfExpr:
+ retval = _equalForPortionOfExpr(a, b);
+ break;
case T_JoinExpr:
retval = _equalJoinExpr(a, b);
break;
@@ -3719,6 +3750,9 @@ equal(const void *a, const void *b)
case T_OnConflictClause:
retval = _equalOnConflictClause(a, b);
break;
+ case T_ForPortionOfClause:
+ retval = _equalForPortionOfClause(a, b);
+ break;
case T_CommonTableExpr:
retval = _equalCommonTableExpr(a, b);
break;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index e084c3f069..c742bb7e8c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -418,9 +418,11 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
WRITE_NODE_FIELD(rowMarks);
WRITE_INT_FIELD(epqParam);
WRITE_ENUM_FIELD(onConflictAction, OnConflictAction);
+ WRITE_NODE_FIELD(forPortionOf);
WRITE_NODE_FIELD(arbiterIndexes);
WRITE_NODE_FIELD(onConflictSet);
WRITE_NODE_FIELD(onConflictWhere);
+ // TODO: add things for ForPortionOf
WRITE_UINT_FIELD(exclRelRTI);
WRITE_NODE_FIELD(exclRelTlist);
}
@@ -1688,6 +1690,23 @@ _outOnConflictExpr(StringInfo str, const OnConflictExpr *node)
WRITE_NODE_FIELD(exclRelTlist);
}
+static void
+_outForPortionOfExpr(StringInfo str, const ForPortionOfExpr *node)
+{
+ WRITE_NODE_TYPE("FORPORTIONOFEXPR");
+
+ WRITE_INT_FIELD(range_attno);
+ WRITE_STRING_FIELD(range_name);
+ WRITE_NODE_FIELD(range);
+ WRITE_NODE_FIELD(startCol);
+ WRITE_NODE_FIELD(endCol);
+ WRITE_NODE_FIELD(targetStart);
+ WRITE_NODE_FIELD(targetEnd);
+ WRITE_NODE_FIELD(targetRange);
+ WRITE_NODE_FIELD(overlapsExpr);
+ WRITE_NODE_FIELD(rangeSet);
+}
+
/*****************************************************************************
*
* Stuff from pathnodes.h.
@@ -3972,6 +3991,9 @@ outNode(StringInfo str, const void *obj)
case T_OnConflictExpr:
_outOnConflictExpr(str, obj);
break;
+ case T_ForPortionOfExpr:
+ _outForPortionOfExpr(str, obj);
+ break;
case T_Path:
_outPath(str, obj);
break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index d5b23a3479..a7acb58764 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1374,6 +1374,27 @@ _readAppendRelInfo(void)
*/
/*
+ * _readForPortionOfExpr
+ */
+static ForPortionOfExpr *
+_readForPortionOfExpr(void)
+{
+ READ_LOCALS(ForPortionOfExpr);
+
+ READ_INT_FIELD(range_attno);
+ READ_STRING_FIELD(range_name);
+ READ_NODE_FIELD(range);
+ READ_NODE_FIELD(startCol);
+ READ_NODE_FIELD(endCol);
+ READ_NODE_FIELD(targetStart);
+ READ_NODE_FIELD(targetEnd);
+ READ_NODE_FIELD(targetRange);
+ READ_NODE_FIELD(overlapsExpr);
+ READ_NODE_FIELD(rangeSet);
+ READ_DONE();
+}
+
+/*
* _readRangeTblEntry
*/
static RangeTblEntry *
@@ -1648,6 +1669,7 @@ _readModifyTable(void)
READ_NODE_FIELD(rowMarks);
READ_INT_FIELD(epqParam);
READ_ENUM_FIELD(onConflictAction, OnConflictAction);
+ READ_NODE_FIELD(forPortionOf);
READ_NODE_FIELD(arbiterIndexes);
READ_NODE_FIELD(onConflictSet);
READ_NODE_FIELD(onConflictWhere);
@@ -2723,6 +2745,8 @@ parseNodeString(void)
return_value = _readOnConflictExpr();
else if (MATCH("APPENDRELINFO", 13))
return_value = _readAppendRelInfo();
+ else if (MATCH("FORPORTIONOFEXPR", 16))
+ return_value = _readForPortionOfExpr();
else if (MATCH("RTE", 3))
return_value = _readRangeTblEntry();
else if (MATCH("RANGETBLFUNCTION", 16))
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index fc25908dc6..7425c84b81 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -289,7 +289,8 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
bool partColsUpdated,
List *resultRelations, List *subplans, List *subroots,
List *withCheckOptionLists, List *returningLists,
- List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+ List *rowMarks, OnConflictExpr *onconflict,
+ ForPortionOfExpr *forPortionOf, int epqParam);
static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
GatherMergePath *best_path);
@@ -2631,6 +2632,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
best_path->returningLists,
best_path->rowMarks,
best_path->onconflict,
+ best_path->forPortionOf,
best_path->epqParam);
copy_generic_path_info(&plan->plan, &best_path->path);
@@ -6624,7 +6626,8 @@ make_modifytable(PlannerInfo *root,
bool partColsUpdated,
List *resultRelations, List *subplans, List *subroots,
List *withCheckOptionLists, List *returningLists,
- List *rowMarks, OnConflictExpr *onconflict, int epqParam)
+ List *rowMarks, OnConflictExpr *onconflict,
+ ForPortionOfExpr *forPortionOf, int epqParam)
{
ModifyTable *node = makeNode(ModifyTable);
List *fdw_private_list;
@@ -6681,6 +6684,7 @@ make_modifytable(PlannerInfo *root,
node->exclRelRTI = onconflict->exclRelIndex;
node->exclRelTlist = onconflict->exclRelTlist;
}
+ node->forPortionOf = (Node *) forPortionOf;
node->withCheckOptionLists = withCheckOptionLists;
node->returningLists = returningLists;
node->rowMarks = rowMarks;
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b44efd6314..7b55718293 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1799,6 +1799,7 @@ inheritance_planner(PlannerInfo *root)
returningLists,
rowMarks,
NULL,
+ parse->forPortionOf,
assign_special_exec_param(root)));
}
@@ -2382,6 +2383,7 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
returningLists,
rowMarks,
parse->onConflict,
+ parse->forPortionOf,
assign_special_exec_param(root));
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index d9ce516211..0fb867ba92 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3450,7 +3450,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
List *subroots,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- int epqParam)
+ ForPortionOfExpr *forPortionOf, int epqParam)
{
ModifyTablePath *pathnode = makeNode(ModifyTablePath);
double total_size;
@@ -3521,6 +3521,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->returningLists = returningLists;
pathnode->rowMarks = rowMarks;
pathnode->onconflict = onconflict;
+ pathnode->forPortionOf = forPortionOf;
pathnode->epqParam = epqParam;
return pathnode;
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6676412842..aa4ade34c4 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -42,9 +42,12 @@
#include "parser/parse_param.h"
#include "parser/parse_relation.h"
#include "parser/parse_target.h"
+#include "parser/parser.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteManip.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
+#include "utils/syscache.h"
/* Hook for plugins to get control at end of parse analysis */
@@ -58,6 +61,9 @@ static List *transformInsertRow(ParseState *pstate, List *exprlist,
bool strip_indirection);
static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
OnConflictClause *onConflictClause);
+static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOfClause);
static int count_rowexpr_columns(ParseState *pstate, Node *expr);
static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt);
static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt);
@@ -69,7 +75,8 @@ static void determineRecursiveColTypes(ParseState *pstate,
static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
static List *transformReturningList(ParseState *pstate, List *returningList);
static List *transformUpdateTargetList(ParseState *pstate,
- List *targetList);
+ List *targetList,
+ ForPortionOfExpr *forPortionOf);
static Query *transformDeclareCursorStmt(ParseState *pstate,
DeclareCursorStmt *stmt);
static Query *transformExplainStmt(ParseState *pstate,
@@ -398,6 +405,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_DELETE;
@@ -436,7 +444,20 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf);
+
+ // TODO: DRY with UPDATE
+ if (stmt->forPortionOf)
+ {
+ if (stmt->whereClause)
+ whereClause = (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, stmt->whereClause), -1);
+ else
+ whereClause = qry->forPortionOf->overlapsExpr;
+ }
+ else
+ whereClause = stmt->whereClause;
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -1044,7 +1065,7 @@ transformOnConflictClause(ParseState *pstate,
* Now transform the UPDATE subexpressions.
*/
onConflictSet =
- transformUpdateTargetList(pstate, onConflictClause->targetList);
+ transformUpdateTargetList(pstate, onConflictClause->targetList, NULL);
onConflictWhere = transformWhereClause(pstate,
onConflictClause->whereClause,
@@ -1066,6 +1087,204 @@ transformOnConflictClause(ParseState *pstate,
return result;
}
+/*
+ * transformForPortionOfClause
+ * transforms a ForPortionOfClause in an UPDATE/DELETE statement
+ */
+static ForPortionOfExpr *
+transformForPortionOfClause(ParseState *pstate,
+ int rtindex,
+ ForPortionOfClause *forPortionOf)
+{
+ Relation targetrel = pstate->p_target_relation;
+ RangeTblEntry *target_rte = pstate->p_target_nsitem->p_rte;
+ char *range_name = forPortionOf->range_name;
+ char *range_type_name;
+ int range_attno;
+ ForPortionOfExpr *result;
+ List *targetList;
+
+ result = makeNode(ForPortionOfExpr);
+
+ /*
+ * First look for a range column, then look for a period.
+ */
+ range_attno = attnameAttNum(targetrel, range_name, true);
+ if (range_attno != InvalidAttrNumber)
+ {
+ Form_pg_attribute attr = TupleDescAttr(targetrel->rd_att, range_attno - 1);
+
+ // TODO: check attr->attisdropped ?
+
+ /* Make sure it's a range column */
+ if (!type_is_range(attr->atttypid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("column \"%s\" of relation \"%s\" is not a range type",
+ range_name,
+ RelationGetRelationName(pstate->p_target_relation)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+
+ /* Make sure the table has a primary key */
+ Oid pkoid = RelationGetPrimaryKeyIndex(targetrel);
+ if (pkoid == InvalidOid)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("relation \"%s\" does not have a temporal primary key",
+ RelationGetRelationName(pstate->p_target_relation)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+
+ /* Make sure the primary key is a temporal key */
+ // TODO: need a lock here?
+ HeapTuple indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(pkoid));
+ if (!HeapTupleIsValid(indexTuple)) /* should not happen */
+ elog(ERROR, "cache lookup failed for index %u", pkoid);
+ Form_pg_index pk = (Form_pg_index) GETSTRUCT(indexTuple);
+ ReleaseSysCache(indexTuple);
+
+ /*
+ * Only temporal pkey indexes have both isprimary and isexclusion.
+ * Checking those saves us from scanning pg_constraint
+ * like in RelationGetExclusionInfo.
+ */
+ if (!(pk->indisprimary && pk->indisexclusion))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("relation \"%s\" does not have a temporal primary key",
+ RelationGetRelationName(pstate->p_target_relation)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+ }
+
+ /* Make sure the range attribute is the last part of the pkey. */
+ if (range_attno != pk->indkey.values[pk->indnkeyatts - 1])
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("column \"%s\" is not the temporal part of the primary key for relation \"%s\"",
+ range_name,
+ RelationGetRelationName(pstate->p_target_relation)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+ }
+
+ Var *v = makeVar(
+ rtindex,
+ range_attno,
+ attr->atttypid,
+ attr->atttypmod,
+ attr->attcollation,
+ 0);
+ v->location = forPortionOf->range_name_location;
+ result->range = (Expr *) v;
+ range_type_name = get_typname(attr->atttypid);
+
+ } else {
+ // TODO: Try to find a period,
+ // and set result->range to an Expr like tsrange(period->start_col, period->end_col)
+ // Probably we can make an A_Expr and call transformExpr on it, right?
+
+ /*
+ * We need to choose a range type based on the period's columns' type.
+ * Normally inferring a range type from an element type is not allowed,
+ * because there might be more than one.
+ * In this case SQL:2011 only has periods for timestamp, timestamptz, and date,
+ * which all have built-in range types.
+ * Let's just take the first range we have for that type,
+ * ordering by oid, so that we get built-in range types first.
+ */
+
+ // TODO: set result->range
+ // TODO: set range_type_name
+ }
+
+ if (range_attno == InvalidAttrNumber)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("column or period \"%s\" of relation \"%s\" does not exist",
+ range_name,
+ RelationGetRelationName(pstate->p_target_relation)),
+ parser_errposition(pstate, forPortionOf->range_name_location)));
+
+ /*
+ * targetStart and End are literal strings
+ * that we'll coerce to the range's element type later.
+ * But if they are "Infinity" or "-Infinity" we should set them to NULL,
+ * because ranges treat NULL as "further" than +/-Infinity.
+ */
+ if (pg_strcasecmp(((A_Const *) forPortionOf->target_start)->val.val.str,
+ "-Infinity") == 0)
+ {
+ A_Const *n = makeNode(A_Const);
+ n->val.type = T_Null;
+ n->location = ((A_Const*)forPortionOf->target_start)->location;
+ result->targetStart = (Node *) n;
+ }
+ else
+ result->targetStart = forPortionOf->target_start;
+
+ if (pg_strcasecmp(((A_Const *) forPortionOf->target_end)->val.val.str,
+ "Infinity") == 0)
+ {
+ A_Const *n = makeNode(A_Const);
+ n->val.type = T_Null;
+ n->location = ((A_Const*)forPortionOf->target_end)->location;
+ result->targetEnd = (Node *) n;
+ }
+ else
+ result->targetEnd = forPortionOf->target_end;
+
+ FuncCall *fc = makeFuncCall(SystemFuncName(range_type_name),
+ list_make2(result->targetStart,
+ result->targetEnd),
+ // TODO: FROM...TO... location instead?:
+ forPortionOf->range_name_location);
+ result->targetRange = transformExpr(pstate, (Node *) fc, EXPR_KIND_UPDATE_PORTION);
+
+ /* overlapsExpr is something we can add to the whereClause */
+ result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, "&&",
+ // TODO: Maybe need a copy here?:
+ (Node *) result->range, (Node *) fc,
+ forPortionOf->range_name_location);
+
+ /*
+ * Now make sure we update the start/end time of the record.
+ * For a range col (r) this is `r = r * targetRange`.
+ * For a PERIOD with cols (s, e) this is `s = lower(tsrange(s, r) * targetRange)`
+ * and `e = upper(tsrange(e, r) * targetRange` (of course not necessarily with
+ * tsrange, but with whatever range type is used there)).
+ *
+ * We also compute the possible left-behind bits at the start and end of the tuple,
+ * so that we can INSERT them if necessary.
+ */
+ targetList = NIL;
+ if (range_attno != InvalidAttrNumber)
+ {
+ Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
+ // TODO: Maybe need a copy here?:
+ (Node *) result->range, (Node *) fc,
+ forPortionOf->range_name_location);
+
+ rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
+ TargetEntry *tle = makeTargetEntry(rangeSetExpr,
+ range_attno,
+ range_name,
+ false);
+
+ targetList = lappend(targetList, tle);
+ } else {
+ /* TODO: Set up targetList for PERIODs */
+ }
+ result->rangeSet = targetList;
+
+ /* Mark the range column as requiring update permissions */
+ target_rte->updatedCols = bms_add_member(target_rte->updatedCols,
+ range_attno - FirstLowInvalidHeapAttributeNumber);
+
+ result->range_attno = range_attno;
+ result->range_name = range_name;
+
+ return result;
+}
/*
* BuildOnConflictExcludedTargetlist
@@ -2221,6 +2440,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_UPDATE;
@@ -2238,6 +2458,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
stmt->relation->inh,
true,
ACL_UPDATE);
+
+ if (stmt->forPortionOf)
+ qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf);
+
nsitem = pstate->p_target_nsitem;
/* subqueries in FROM cannot access the result relation */
@@ -2254,7 +2478,16 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
nsitem->p_lateral_only = false;
nsitem->p_lateral_ok = true;
- qual = transformWhereClause(pstate, stmt->whereClause,
+ if (stmt->forPortionOf)
+ {
+ if (stmt->whereClause)
+ whereClause = (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, stmt->whereClause), -1);
+ else
+ whereClause = qry->forPortionOf->overlapsExpr;
+ }
+ else
+ whereClause = stmt->whereClause;
+ qual = transformWhereClause(pstate, whereClause,
EXPR_KIND_WHERE, "WHERE");
qry->returningList = transformReturningList(pstate, stmt->returningList);
@@ -2263,7 +2496,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* Now we are done with SELECT-like processing, and can get on with
* transforming the target list to match the UPDATE target columns.
*/
- qry->targetList = transformUpdateTargetList(pstate, stmt->targetList);
+ qry->targetList = transformUpdateTargetList(pstate, stmt->targetList, qry->forPortionOf);
qry->rtable = pstate->p_rtable;
qry->jointree = makeFromExpr(pstate->p_joinlist, qual);
@@ -2281,7 +2514,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
* handle SET clause in UPDATE/INSERT ... ON CONFLICT UPDATE
*/
static List *
-transformUpdateTargetList(ParseState *pstate, List *origTlist)
+transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf)
{
List *tlist = NIL;
RangeTblEntry *target_rte;
@@ -2332,6 +2565,9 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
RelationGetRelationName(pstate->p_target_relation)),
parser_errposition(pstate, origTarget->location)));
+ /* TODO: Make sure user isn't trying to SET the range attribute directly --- TODO or permit it?? */
+
+
updateTargetListEntry(pstate, tle, origTarget->name,
attrno,
origTarget->indirection,
@@ -2348,6 +2584,23 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist)
fill_extraUpdatedCols(target_rte, tupdesc);
+ /*
+ * Record in extraUpdatedCols the temporal bounds if using FOR PORTION OF.
+ * Since these are part of the primary key this ensures we get the right lock type,
+ * and it also tells column-specific triggers on those columns to fire.
+ */
+ if (forPortionOf)
+ {
+ foreach(tl, forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(tl);
+ // TODO: I probably don't want to do this until after rewriting, or maybe not even until the ModifyTable node (which seems to be how generated columns work).
+ tlist = lappend(tlist, tle);
+ target_rte->extraUpdatedCols = bms_add_member(target_rte->extraUpdatedCols,
+ tle->resno - FirstLowInvalidHeapAttributeNumber);
+ }
+ }
+
return tlist;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7e384f956c..aac79175ad 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -229,6 +229,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
RangeVar *range;
IntoClause *into;
WithClause *with;
+ ForPortionOfClause *forportionof;
InferClause *infer;
OnConflictClause *onconflict;
A_Indices *aind;
@@ -475,10 +476,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 where_clause where_or_current_clause
+%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
ExclusionWhereClause operator_def_arg
+%type opt_column_and_period_list
%type rowsfrom_item rowsfrom_list opt_col_def_list
%type opt_ordinality
%type ExclusionConstraintList ExclusionConstraintElem
@@ -498,6 +501,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type joined_table
%type relation_expr
%type relation_expr_opt_alias
+%type for_portion_of_clause
%type tablesample_clause opt_repeatable_clause
%type target_el set_target insert_column_item
@@ -672,8 +676,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
ORDER ORDINALITY OTHERS OUT_P OUTER_P
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
- PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY
- POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
+ PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PERIOD PLACING PLANS POLICY
+ PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
QUOTE
@@ -3486,6 +3490,7 @@ ColConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NULL;
+ n->without_overlaps = NULL;
n->options = $3;
n->indexname = NULL;
n->indexspace = $4;
@@ -3702,18 +3707,19 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *)n;
}
- | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | PRIMARY KEY '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = $4;
- n->including = $6;
- n->options = $7;
+ n->without_overlaps = $5;
+ n->including = $7;
+ n->options = $8;
n->indexname = NULL;
- n->indexspace = $8;
- processCASbits($9, @9, "PRIMARY KEY",
+ n->indexspace = $9;
+ processCASbits($10, @10, "PRIMARY KEY",
&n->deferrable, &n->initdeferred, NULL,
NULL, yyscanner);
$$ = (Node *)n;
@@ -3724,6 +3730,7 @@ ConstraintElem:
n->contype = CONSTR_PRIMARY;
n->location = @1;
n->keys = NIL;
+ n->without_overlaps = NULL;
n->including = NIL;
n->options = NIL;
n->indexname = $3;
@@ -3752,19 +3759,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);
@@ -3787,6 +3796,21 @@ columnList:
| columnList ',' columnElem { $$ = lappend($1, $3); }
;
+withoutOverlapsClause:
+ ',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+ | /*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);
@@ -11124,13 +11148,15 @@ returning_clause:
*****************************************************************************/
DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias
+ for_portion_of_clause
using_clause where_or_current_clause returning_clause
{
DeleteStmt *n = makeNode(DeleteStmt);
n->relation = $4;
- n->usingClause = $5;
- n->whereClause = $6;
- n->returningList = $7;
+ n->forPortionOf = $5;
+ n->usingClause = $6;
+ n->whereClause = $7;
+ n->returningList = $8;
n->withClause = $1;
$$ = (Node *)n;
}
@@ -11193,6 +11219,7 @@ opt_nowait_or_skip:
*****************************************************************************/
UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
+ for_portion_of_clause
SET set_clause_list
from_clause
where_or_current_clause
@@ -11200,10 +11227,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias
{
UpdateStmt *n = makeNode(UpdateStmt);
n->relation = $3;
- n->targetList = $5;
- n->fromClause = $6;
- n->whereClause = $7;
- n->returningList = $8;
+ n->forPortionOf = $4;
+ n->targetList = $6;
+ n->fromClause = $7;
+ n->whereClause = $8;
+ n->returningList = $9;
n->withClause = $1;
$$ = (Node *)n;
}
@@ -12308,6 +12336,20 @@ relation_expr_opt_alias: relation_expr %prec UMINUS
}
;
+for_portion_of_clause:
+ FOR PORTION OF ColId FROM Sconst TO Sconst
+ {
+ ForPortionOfClause *n = makeNode(ForPortionOfClause);
+ n->range_name = $4;
+ n->range_name_location = @4;
+ n->target_start = makeStringConst($6, @6);
+ n->target_end = makeStringConst($8, @8);
+ $$ = n;
+ }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
+
/*
* TABLESAMPLE decoration in a FROM item
*/
@@ -15332,6 +15374,7 @@ unreserved_keyword:
| PASSWORD
| PLANS
| POLICY
+ | PORTION
| PRECEDING
| PREPARE
| PREPARED
@@ -15612,6 +15655,7 @@ reserved_keyword:
| ONLY
| OR
| ORDER
+ | PERIOD
| PLACING
| PRIMARY
| REFERENCES
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 831db4af95..5c4da05240 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -3518,6 +3518,8 @@ ParseExprKindName(ParseExprKind exprKind)
case EXPR_KIND_UPDATE_SOURCE:
case EXPR_KIND_UPDATE_TARGET:
return "UPDATE";
+ case EXPR_KIND_UPDATE_PORTION:
+ return "FOR PORTION OF";
case EXPR_KIND_GROUP_BY:
return "GROUP BY";
case EXPR_KIND_ORDER_BY:
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index af77f1890f..f3eaa9de58 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1414,6 +1414,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->oldNode = InvalidOid;
index->unique = idxrec->indisunique;
index->primary = idxrec->indisprimary;
+ index->istemporal = idxrec->indisprimary && idxrec->indisexclusion;
index->transformed = true; /* don't need transformIndexStmt */
index->concurrent = false;
index->if_not_exists = false;
@@ -1973,7 +1974,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
index = makeNode(IndexStmt);
- index->unique = (constraint->contype != CONSTR_EXCLUSION);
+ index->unique = (constraint->contype != CONSTR_EXCLUSION && constraint->without_overlaps == NULL);
index->primary = (constraint->contype == CONSTR_PRIMARY);
if (index->primary)
{
@@ -1991,6 +1992,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
*/
}
index->isconstraint = true;
+ index->istemporal = constraint->without_overlaps != NULL;
index->deferrable = constraint->deferrable;
index->initdeferred = constraint->initdeferred;
@@ -2364,6 +2366,153 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
notnullcmd->name = pstrdup(key);
notnullcmds = lappend(notnullcmds, notnullcmd);
}
+
+ if (constraint->without_overlaps != NULL)
+ {
+ /*
+ * We are building the index like for an EXCLUSION constraint,
+ * so use the equality operator for these elements.
+ */
+ List *opname = list_make1(makeString("="));
+ index->excludeOpNames = lappend(index->excludeOpNames, opname);
+ }
+ }
+
+ /*
+ * Anything in without_overlaps should be included,
+ * but with the overlaps operator (&&) instead of equality.
+ */
+ if (constraint->without_overlaps != NULL) {
+ // char *without_overlaps_str = nodeToString(constraint->without_overlaps);
+ char *without_overlaps_str = strVal(constraint->without_overlaps);
+ IndexElem *iparam = makeNode(IndexElem);
+
+ /*
+ * Iterate through the table's columns
+ * (like just a little bit above).
+ * If we find one whose name is the same as without_overlaps,
+ * validate that it's a range type.
+ *
+ * Otherwise iterate through the table's non-system PERIODs,
+ * and if we find one then use its start/end columns
+ * to construct a range expression.
+ *
+ * Otherwise report an error.
+ */
+ bool found = false;
+ ColumnDef *column = NULL;
+ ListCell *columns;
+ if (cxt->isalter)
+ {
+ // TODO: DRY this up with the non-ALTER case:
+ Relation rel = cxt->rel;
+ /*
+ * Look up columns on existing table.
+ */
+ for (int i = 0; i < rel->rd_att->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+ const char *attname = NameStr(attr->attname);
+ if (strcmp(attname, without_overlaps_str) == 0)
+ {
+ if (type_is_range(attr->atttypid))
+ {
+ found = true;
+ break;
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type",
+ without_overlaps_str)));
+ }
+ }
+ }
+ }
+ else
+ {
+ /*
+ * Look up columns on the being-created table.
+ */
+ foreach(columns, cxt->columns)
+ {
+ column = castNode(ColumnDef, lfirst(columns));
+ if (strcmp(column->colname, without_overlaps_str) == 0)
+ {
+ Oid colTypeOid = typenameTypeId(NULL, column->typeName);
+ if (type_is_range(colTypeOid))
+ {
+ found = true;
+ break;
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type",
+ without_overlaps_str)));
+ }
+ }
+ }
+ }
+ if (found)
+ {
+ AlterTableCmd *notnullcmd;
+ iparam->name = pstrdup(without_overlaps_str);
+ iparam->expr = NULL;
+
+ /*
+ * Force the column to NOT NULL since it is part of the primary key.
+ */
+ notnullcmd = makeNode(AlterTableCmd);
+
+ notnullcmd->subtype = AT_SetNotNull;
+ notnullcmd->name = pstrdup(without_overlaps_str);
+ notnullcmds = lappend(notnullcmds, notnullcmd);
+ }
+ else {
+ found = false;
+ /*
+ * TODO: Search for a non-system PERIOD with the right name.
+ */
+ if (found)
+ {
+ iparam->name = NULL;
+ /*
+ * TODO: Build up a parse tree to cast the period to a range.
+ * See transformExpr (called below and defined in parser/parse_expr.c.
+ */
+ /*
+ TypeCast *expr = makeNode(TypeCast);
+ expr->arg = constraint->without_overlaps;
+ expr->typeName = "...."; // TODO: need to look up which range type to use
+ expr->location = -1;
+ iparam->expr = transformExpr(..., expr, EXPR_KIND_INDEX_EXPRESSION);
+ */
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("range or PERIOD \"%s\" named in WITHOUT OVERLAPS does not exist",
+ without_overlaps_str)));
+ }
+ }
+ {
+ List *opname;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+
+ opname = list_make1(makeString("&&"));
+ index->excludeOpNames = lappend(index->excludeOpNames, opname);
+ index->accessMethod = "gist";
+ constraint->access_method = "gist";
+ }
}
}
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 3b4f28874a..17b23c4bd1 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2990,6 +2990,18 @@ rewriteTargetView(Query *parsetree, Relation view)
}
}
+ if (parsetree->forPortionOf)
+ {
+ foreach(lc, parsetree->forPortionOf->rangeSet)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+ if (!tle->resjunk)
+ modified_cols = bms_add_member(modified_cols,
+ tle->resno - FirstLowInvalidHeapAttributeNumber);
+ }
+ }
+
auto_update_detail = view_cols_are_auto_updatable(viewquery,
modified_cols,
NULL,
diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c
index 9d1ca13e32..c65b82b41c 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -1226,6 +1226,48 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT
return false;
}
+/*
+ * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1
+ * after subtracting r2, or if nothing is left then to the empty range.
+ * output1 will always be "before" r2 and output2 "after".
+ */
+void
+range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1, RangeType **output2)
+{
+ RangeBound lower1,
+ lower2;
+ RangeBound upper1,
+ upper2;
+ bool empty1,
+ empty2;
+
+ range_deserialize(typcache, r1, &lower1, &upper1, &empty1);
+ range_deserialize(typcache, r2, &lower2, &upper2, &empty2);
+
+ if (range_cmp_bounds(typcache, &lower1, &lower2) < 0)
+ {
+ lower2.inclusive = !lower2.inclusive;
+ lower2.lower = false;
+ *output1 = make_range(typcache, &lower1, &lower2, false);
+ }
+ else
+ {
+ *output1 = make_empty_range(typcache);
+ }
+
+ if (range_cmp_bounds(typcache, &upper1, &upper2) > 0)
+ {
+ upper2.inclusive = !upper2.inclusive;
+ upper2.lower = true;
+ *output2 = make_range(typcache, &upper2, &upper1, false);
+ }
+ else
+ {
+ *output2 = make_empty_range(typcache);
+ }
+}
+
/* range -> range aggregate functions */
Datum
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 4ab7cda110..d50bf47ba3 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -108,6 +108,7 @@ 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 */
int16 pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */
int16 fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */
@@ -191,7 +192,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);
@@ -351,18 +352,46 @@ 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 range_agg(r) AS r
+ * FROM (
+ * SELECT pkperiodatt AS r
+ * FROM [ONLY] pktable x
+ * WHERE pkatt1 = $1 [AND ...]
+ * FOR KEY SHARE OF x
+ * ) x1
+ * ) x2
+ * WHERE $n <@ x2.r
+ * Note if FOR KEY SHARE ever allows aggregate functions
+ * we can make this a bit simpler.
* ----------
*/
initStringInfo(&querybuf);
pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ?
"" : "ONLY ";
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
- pk_only, pkrelname);
+ if (riinfo->temporal)
+ {
+ quoteOneName(attname,
+ RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]));
+ appendStringInfo(&querybuf,
+ "SELECT 1 FROM (SELECT range_agg(r) AS r FROM (SELECT %s AS r FROM %s%s x",
+ attname, pk_only, pkrelname);
+ }
+ else {
+ appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x",
+ pk_only, pkrelname);
+ }
querysep = "WHERE";
for (int i = 0; i < riinfo->nkeys; i++)
{
@@ -380,6 +409,8 @@ RI_FKey_check(TriggerData *trigdata)
queryoids[i] = fk_type;
}
appendStringInfoString(&querybuf, " FOR KEY SHARE OF x");
+ if (riinfo->temporal)
+ appendStringInfo(&querybuf, ") x1) x2 WHERE $%d <@ x2.r", riinfo->nkeys);
/* Prepare and save the plan */
qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids,
@@ -1174,7 +1205,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel,
return false;
/* If all old and new key values are equal, no check is needed */
- if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true))
+ if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true))
return false;
/* Else we need to fire the trigger. */
@@ -1267,13 +1298,135 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
return true;
/* If all old and new key values are equal, no check is needed */
- if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false))
+ if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false))
return false;
/* Else we need to fire the trigger. */
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_noaction_upd -
+ *
+ * Give an error and roll back the current transaction if the
+ * update has resulted in a violation of the given referential
+ * integrity constraint.
+ */
+Datum
+TRI_FKey_noaction_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with RESTRICT/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, true);
+}
+
+/*
+ * TRI_FKey_restrict_upd -
+ *
+ * Restrict update of PK to rows unreferenced by foreign key.
+ *
+ * The SQL standard intends that this referential action occur exactly when
+ * the update is performed, rather than after. This appears to be
+ * the only difference between "NO ACTION" and "RESTRICT". In Postgres
+ * we still implement this as an AFTER trigger, but it's non-deferrable.
+ */
+Datum
+TRI_FKey_restrict_upd(PG_FUNCTION_ARGS)
+{
+ /* Check that this is a valid trigger call on the right time and event. */
+ ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE);
+
+ /* Share code with NO ACTION/DELETE cases. */
+ return ri_restrict((TriggerData *) fcinfo->context, false);
+}
+
+
/*
* RI_Initial_Check -
*
@@ -2049,6 +2202,7 @@ ri_LoadConstraintInfo(Oid constraintOid)
riinfo->confupdtype = conForm->confupdtype;
riinfo->confdeltype = conForm->confdeltype;
riinfo->confmatchtype = conForm->confmatchtype;
+ riinfo->temporal = conForm->contemporal;
DeconstructFkConstraintRow(tup,
&riinfo->nkeys,
@@ -2643,9 +2797,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
@@ -2653,7 +2810,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;
@@ -2686,29 +2843,43 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot,
if (rel_is_pk)
{
- /*
- * If we are looking at the PK table, then do a bytewise
- * comparison. We must propagate PK changes if the value is
- * changed to one that "looks" different but would compare as
- * equal using the equality operator. This only makes a
- * difference for ON UPDATE CASCADE, but for consistency we treat
- * all changes to the PK the same.
- */
- Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
+ if (riinfo->temporal)
+ {
+ return DatumGetBool(DirectFunctionCall2(range_contains, newvalue, oldvalue));
+ }
+ else
+ {
+ /*
+ * If we are looking at the PK table, then do a bytewise
+ * comparison. We must propagate PK changes if the value is
+ * changed to one that "looks" different but would compare as
+ * equal using the equality operator. This only makes a
+ * difference for ON UPDATE CASCADE, but for consistency we treat
+ * all changes to the PK the same.
+ */
+ Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1);
- if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
- return false;
+ if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen))
+ return false;
+ }
}
else
{
- /*
- * For the FK table, compare with the appropriate equality
- * operator. Changes that compare equal will still satisfy the
- * constraint after the update.
- */
- if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
- oldvalue, newvalue))
- return false;
+ if (riinfo->temporal)
+ {
+ return DatumGetBool(DirectFunctionCall2(range_contains, oldvalue, newvalue));
+ }
+ else
+ {
+ /*
+ * For the FK table, compare with the appropriate equality
+ * operator. Changes that compare equal will still satisfy the
+ * constraint after the update.
+ */
+ if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]),
+ oldvalue, newvalue))
+ return false;
+ }
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5e63238f03..e7d618ca53 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -331,7 +331,7 @@ static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
static int decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf);
+ bool withoutOverlaps, bool withPeriod, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -1998,6 +1998,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
{
Datum val;
bool isnull;
+ bool hasperiod;
const char *string;
/* Start off the constraint definition */
@@ -2010,7 +2011,13 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
elog(ERROR, "null conkey for constraint %u",
constraintId);
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ /*
+ * If it is a temporal foreign key
+ * then it uses PERIOD.
+ */
+ hasperiod = DatumGetBool(SysCacheGetAttr(CONSTROID, tup,
+ Anum_pg_constraint_contemporal, &isnull));
+ decompile_column_index_array(val, conForm->conrelid, false, hasperiod, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2024,7 +2031,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
elog(ERROR, "null confkey for constraint %u",
constraintId);
- decompile_column_index_array(val, conForm->confrelid, &buf);
+ decompile_column_index_array(val, conForm->confrelid, false, hasperiod, &buf);
appendStringInfoChar(&buf, ')');
@@ -2125,7 +2132,13 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
elog(ERROR, "null conkey for constraint %u",
constraintId);
- keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+ /*
+ * If it has exclusion-style operator OIDs
+ * then it uses WITHOUT OVERLAPS.
+ */
+ SysCacheGetAttr(CONSTROID, tup,
+ Anum_pg_constraint_conexclop, &isnull);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, !isnull, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2327,7 +2340,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
*/
static int
decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf)
+ bool withoutOverlaps, bool withPeriod, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2345,9 +2358,21 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
colName = get_attname(relId, DatumGetInt16(keys[j]), false);
if (j == 0)
+ {
appendStringInfoString(buf, quote_identifier(colName));
+ }
+ else if (withoutOverlaps && j == nKeys - 1)
+ {
+ appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
+ }
+ else if (withPeriod && j == nKeys - 1)
+ {
+ appendStringInfo(buf, ", PERIOD %s", quote_identifier(colName));
+ }
else
+ {
appendStringInfo(buf, ", %s", quote_identifier(colName));
+ }
}
return nKeys;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 3ad1a9aa71..d7401ffed5 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1967,6 +1967,32 @@ get_typisdefined(Oid typid)
}
/*
+ * get_typname
+ *
+ * Returns the name of a given type
+ *
+ * Returns a palloc'd copy of the string, or NULL if no such type.
+ */
+char *
+get_typname(Oid typid)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(typtup->typname));
+ ReleaseSysCache(tp);
+ return result;
+ }
+ else
+ return NULL;
+}
+
+/*
* get_typlen
*
* Given the type OID, return the length of the type.
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index ff70326474..6489e1b626 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4386,11 +4386,17 @@ RelationGetIndexList(Relation relation)
* interesting for either oid indexes or replication identity indexes,
* so don't check them.
*/
- if (!index->indisvalid || !index->indisunique ||
- !index->indimmediate ||
+ if (!index->indisvalid || !index->indimmediate ||
!heap_attisnull(htup, Anum_pg_index_indpred, NULL))
continue;
+ /*
+ * Non-unique indexes aren't interesting either,
+ * except when they are temporal primary keys.
+ */
+ if (!index->indisunique && !index->indisprimary)
+ continue;
+
/* remember primary key index if any */
if (index->indisprimary)
pkeyIndex = index->indexrelid;
@@ -4992,8 +4998,9 @@ restart:
* RelationGetExclusionInfo -- get info about index's exclusion constraint
*
* This should be called only for an index that is known to have an
- * associated exclusion constraint. It returns arrays (palloc'd in caller's
- * context) of the exclusion operator OIDs, their underlying functions'
+ * associated exclusion constraint or temporal primary key.
+ * It returns arrays (palloc'd in caller's * context)
+ * of the exclusion operator OIDs, their underlying functions'
* OIDs, and their strategy numbers in the index's opclasses. We cache
* all this information since it requires a fair amount of work to get.
*/
@@ -5059,7 +5066,12 @@ RelationGetExclusionInfo(Relation indexRelation,
int nelem;
/* We want the exclusion constraint owning the index */
- if (conform->contype != CONSTRAINT_EXCLUSION ||
+ /*
+ * TODO: Is this too permissive?
+ * Maybe it needs to be (!= CONSTRAINT_PRIMARY || !has_excl_operators)
+ */
+ if ((conform->contype != CONSTRAINT_EXCLUSION &&
+ conform->contype != CONSTRAINT_PRIMARY) ||
conform->conindid != RelationGetRelid(indexRelation))
continue;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4cbb37511c..02f8351f1a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6907,7 +6907,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_tablespace,
i_indreloptions,
i_indstatcols,
- i_indstatvals;
+ i_indstatvals,
+ i_withoutoverlaps;
int ntups;
for (i = 0; i < numTables; i++)
@@ -6968,7 +6969,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT pg_catalog.array_agg(attstattarget ORDER BY attnum) "
" FROM pg_catalog.pg_attribute "
" WHERE attrelid = i.indexrelid AND "
- " attstattarget >= 0) AS indstatvals "
+ " attstattarget >= 0) AS indstatvals, "
+ "c.conexclop IS NOT NULL AS withoutoverlaps "
"FROM pg_catalog.pg_index i "
"JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
"JOIN pg_catalog.pg_class t2 ON (t2.oid = i.indrelid) "
@@ -7007,7 +7009,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
"t.reloptions AS indreloptions, "
"'' AS indstatcols, "
- "'' AS indstatvals "
+ "'' AS indstatvals, "
+ "null AS withoutoverlaps "
"FROM pg_catalog.pg_index i "
"JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
"LEFT JOIN pg_catalog.pg_constraint c "
@@ -7042,7 +7045,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
"t.reloptions AS indreloptions, "
"'' AS indstatcols, "
- "'' AS indstatvals "
+ "'' AS indstatvals, "
+ "null AS withoutoverlaps "
"FROM pg_catalog.pg_index i "
"JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
"LEFT JOIN pg_catalog.pg_constraint c "
@@ -7073,7 +7077,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
"t.reloptions AS indreloptions, "
"'' AS indstatcols, "
- "'' AS indstatvals "
+ "'' AS indstatvals, "
+ "null AS withoutoverlaps "
"FROM pg_catalog.pg_index i "
"JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
"LEFT JOIN pg_catalog.pg_depend d "
@@ -7107,7 +7112,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
"(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, "
"null AS indreloptions, "
"'' AS indstatcols, "
- "'' AS indstatvals "
+ "'' AS indstatvals, "
+ "null AS withoutoverlaps "
"FROM pg_catalog.pg_index i "
"JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) "
"LEFT JOIN pg_catalog.pg_depend d "
@@ -7147,6 +7153,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_indreloptions = PQfnumber(res, "indreloptions");
i_indstatcols = PQfnumber(res, "indstatcols");
i_indstatvals = PQfnumber(res, "indstatvals");
+ i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");
tbinfo->indexes = indxinfo =
(IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
@@ -7207,6 +7214,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
constrinfo[j].condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
constrinfo[j].conislocal = true;
constrinfo[j].separate = true;
+ constrinfo[j].withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
indxinfo[j].indexconstraint = constrinfo[j].dobj.dumpId;
}
@@ -16729,9 +16737,22 @@ dumpConstraint(Archive *fout, ConstraintInfo *coninfo)
break;
attname = getAttrName(indkey, tbinfo);
- appendPQExpBuffer(q, "%s%s",
- (k == 0) ? "" : ", ",
- fmtId(attname));
+ if (k == 0)
+ {
+ appendPQExpBuffer(q, "%s",
+ fmtId(attname));
+ }
+ else if (k == indxinfo->indnkeyattrs - 1 &&
+ coninfo->withoutoverlaps)
+ {
+ appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
+ fmtId(attname));
+ }
+ else
+ {
+ appendPQExpBuffer(q, ", %s",
+ fmtId(attname));
+ }
}
if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 52f2c12381..cc2e8588ac 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -449,6 +449,7 @@ typedef struct _constraintInfo
bool condeferred; /* true if constraint is INITIALLY DEFERRED */
bool conislocal; /* true if constraint has local definition */
bool separate; /* true if must dump as separate item */
+ bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
typedef struct _procLangInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1b90cbd9b5..9d3f752948 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -634,6 +634,28 @@ my %tests = (
},
},
+ 'ALTER TABLE ONLY test_table ADD CONSTRAINT ... PRIMARY KEY (..., ... WITHOUT OVERLAPS)' => {
+ create_sql => 'CREATE TABLE dump_test.test_table_tpk (
+ col1 int4range,
+ col2 tstzrange,
+ CONSTRAINT test_table_tpk_pkey PRIMARY KEY
+ (col1, col2 WITHOUT OVERLAPS));',
+ regexp => qr/^
+ \QALTER TABLE ONLY dump_test.test_table_tpk\E \n^\s+
+ \QADD CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS);\E
+ /xm,
+ like => {
+ %full_runs,
+ %dump_test_schema_runs,
+ section_post_data => 1,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ },
+ },
+
'ALTER TABLE (partitioned) ADD CONSTRAINT ... FOREIGN KEY' => {
create_order => 4,
create_sql => 'CREATE TABLE dump_test.test_table_fk (
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f3c7eb96fa..644adb995d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2386,6 +2386,8 @@ describeOneTableDetails(const char *schemaname,
}
/* Everything after "USING" is echoed verbatim */
+ // TODO: Show WITHOUT OVERLAPS info here?
+ // It is not really part of the *index*.
indexdef = PQgetvalue(result, i, 5);
usingpos = strstr(indexdef, " USING ");
if (usingpos)
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index a2890c1314..d42da26347 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -77,6 +77,7 @@ extern Oid index_create(Relation heapRelation,
#define INDEX_CONSTR_CREATE_INIT_DEFERRED (1 << 2)
#define INDEX_CONSTR_CREATE_UPDATE_INDEX (1 << 3)
#define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4)
+#define INDEX_CONSTR_CREATE_TEMPORAL (1 << 5)
extern Oid index_concurrently_create_copy(Relation heapRelation,
Oid oldIndexId,
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 9600ece93c..1740578e37 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -102,6 +102,12 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
/* Has a local definition and cannot be inherited */
bool connoinherit;
+ /*
+ * For primary and foreign keys, signifies the last column is a range
+ * and should use overlaps instead of equals.
+ */
+ bool contemporal;
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/*
@@ -116,26 +122,26 @@ 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];
/*
- * 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];
/*
- * 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];
/*
* If an exclusion constraint, the OIDs of the exclusion operators for
- * each column of the constraint
+ * each column of the constraint. Also set for temporal primary keys.
*/
Oid conexclop[1];
@@ -210,6 +216,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
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 27f25d989b..05eaca30c3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3726,6 +3726,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' },
+
{ oid => '1666',
proname => 'varbiteq', proleakproof => 't', prorettype => 'bool',
proargtypes => 'varbit varbit', prosrc => 'biteq' },
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index cd3ddf781f..517e782a31 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -25,12 +25,14 @@
#include "storage/condition_variable.h"
#include "utils/hsearch.h"
#include "utils/queryenvironment.h"
+#include "utils/rangetypes.h"
#include "utils/reltrigger.h"
#include "utils/sharedtuplestore.h"
#include "utils/snapshot.h"
#include "utils/sortsupport.h"
#include "utils/tuplesort.h"
#include "utils/tuplestore.h"
+#include "utils/typcache.h"
struct PlanState; /* forward references in this file */
struct PartitionRoutingInfo;
@@ -387,6 +389,22 @@ typedef struct OnConflictSetState
} OnConflictSetState;
/*
+ * ForPortionOfState
+ *
+ * Executor state of a FOR PORTION OF operation.
+ */
+typedef struct ForPortionOfState
+{
+ NodeTag type;
+
+ TypeCacheEntry *fp_rangetypcache; /* type cache entry of the range */
+ RangeType *fp_targetRange; /* the range from FOR PORTION OF */
+ TupleTableSlot *fp_Existing; /* slot to store existing target tuple in */
+ TupleTableSlot *fp_Leftover1; /* slot to store leftover below the target range */
+ TupleTableSlot *fp_Leftover2; /* slot to store leftover above the target range */
+} ForPortionOfState;
+
+/*
* ResultRelInfo
*
* Whenever we update an existing relation, we have to update indexes on the
@@ -475,6 +493,9 @@ typedef struct ResultRelInfo
/* ON CONFLICT evaluation state */
OnConflictSetState *ri_onConflict;
+ /* FOR PORTION OF evaluation state */
+ ForPortionOfState *ri_forPortionOf;
+
/* partition check expression */
List *ri_PartitionCheck;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 8a76afe8cc..8f28305964 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -35,6 +35,7 @@ typedef enum NodeTag
T_ProjectionInfo,
T_JunkFilter,
T_OnConflictSetState,
+ T_ForPortionOfState,
T_ResultRelInfo,
T_EState,
T_TupleTableSlot,
@@ -195,6 +196,7 @@ typedef enum NodeTag
T_JoinExpr,
T_FromExpr,
T_OnConflictExpr,
+ T_ForPortionOfExpr,
T_IntoClause,
/*
@@ -469,6 +471,7 @@ typedef enum NodeTag
T_WithClause,
T_InferClause,
T_OnConflictClause,
+ T_ForPortionOfClause,
T_CommonTableExpr,
T_RoleSpec,
T_TriggerTransition,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 2039b42449..9bd5dec0a6 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -122,6 +122,8 @@ typedef struct Query
int resultRelation; /* rtable index of target relation for
* INSERT/UPDATE/DELETE; 0 for SELECT */
+ ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
+
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
bool hasTargetSRFs; /* has set-returning functions in tlist */
@@ -1384,6 +1386,19 @@ typedef struct RowMarkClause
} RowMarkClause;
/*
+ * ForPortionOfClause
+ * representation of FOR PORTION OF FROM TO
+ */
+typedef struct ForPortionOfClause
+{
+ NodeTag type;
+ char *range_name;
+ int range_name_location;
+ Node *target_start;
+ Node *target_end;
+} ForPortionOfClause;
+
+/*
* WithClause -
* representation of WITH clause
*
@@ -1538,12 +1553,13 @@ typedef struct InsertStmt
*/
typedef struct DeleteStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to delete from */
- List *usingClause; /* optional using clause for more tables */
- Node *whereClause; /* qualifications */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to delete from */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *usingClause; /* optional using clause for more tables */
+ Node *whereClause; /* qualifications */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} DeleteStmt;
/* ----------------------
@@ -1552,13 +1568,14 @@ typedef struct DeleteStmt
*/
typedef struct UpdateStmt
{
- NodeTag type;
- RangeVar *relation; /* relation to update */
- List *targetList; /* the target list (of ResTarget) */
- Node *whereClause; /* qualifications */
- List *fromClause; /* optional from clause for more tables */
- List *returningList; /* list of expressions to return */
- WithClause *withClause; /* WITH clause */
+ NodeTag type;
+ RangeVar *relation; /* relation to update */
+ ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */
+ List *targetList; /* the target list (of ResTarget) */
+ Node *whereClause; /* qualifications */
+ List *fromClause; /* optional from clause for more tables */
+ List *returningList; /* list of expressions to return */
+ WithClause *withClause; /* WITH clause */
} UpdateStmt;
/* ----------------------
@@ -2182,7 +2199,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 */
@@ -2190,6 +2209,9 @@ typedef struct Constraint
Oid old_pktable_oid; /* pg_constraint.confrelid of my former
* self */
+ /* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
+ Node *without_overlaps; /* String node naming PERIOD or range column */
+
/* Fields used for constraints that allow a NOT VALID specification */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
@@ -2785,6 +2807,7 @@ typedef struct IndexStmt
bool unique; /* is index unique? */
bool primary; /* is index a primary key? */
bool isconstraint; /* is it for a pkey/unique constraint? */
+ bool istemporal; /* is it for a temporal pkey? */
bool deferrable; /* is the constraint DEFERRABLE? */
bool initdeferred; /* is the constraint INITIALLY DEFERRED? */
bool transformed; /* true when transformIndexStmt is finished */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 0ceb809644..e1e6145fd1 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1785,6 +1785,7 @@ typedef struct ModifyTablePath
List *returningLists; /* per-target-table RETURNING tlists */
List *rowMarks; /* PlanRowMarks (non-locking only) */
OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */
+ ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
} ModifyTablePath;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4869fe7b6d..9b014ce9e0 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -234,6 +234,8 @@ typedef struct ModifyTable
List *rowMarks; /* PlanRowMarks (non-locking only) */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
OnConflictAction onConflictAction; /* ON CONFLICT action */
+ // TODO: Instead of re-using Expr here, break it into pieces like onConflict{Action,Set,Where}?
+ Node *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */
List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */
List *onConflictSet; /* SET for INSERT ON CONFLICT DO UPDATE */
Node *onConflictWhere; /* WHERE for ON CONFLICT UPDATE */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d73be2ad46..e19dc9fd4e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1538,4 +1538,25 @@ typedef struct OnConflictExpr
List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
} OnConflictExpr;
+/*----------
+ * ForPortionOfExpr - represents a FOR PORTION OF ... expression
+ *
+ * TODO: more notes as needed
+ *----------
+ */
+typedef struct ForPortionOfExpr
+{
+ NodeTag type;
+ int range_attno; /* Range column number */
+ char *range_name; /* Range name */
+ Expr *range; /* Range column or expression */
+ Node *startCol; /* Start column if using a PERIOD */
+ Node *endCol; /* End column if using a PERIOD */
+ Node *targetStart; /* Same type as the range's elements */
+ Node *targetEnd; /* Same type as the range's elements */
+ Node *targetRange; /* A range from targetStart to targetEnd */
+ Node *overlapsExpr; /* range && targetRange */
+ List *rangeSet; /* List of TargetEntrys to set the time column(s) */
+} ForPortionOfExpr;
+
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index e450fe112a..08eb937346 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -258,7 +258,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
List *subroots,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- int epqParam);
+ ForPortionOfExpr *forPortionOf, int epqParam);
extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
Path *subpath,
Node *limitOffset, Node *limitCount,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index b1184c2d15..5e5618ec3e 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -300,9 +300,11 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD)
+PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD)
PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD)
PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD)
PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index d25819aa28..9911d96120 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -55,6 +55,7 @@ typedef enum ParseExprKind
EXPR_KIND_INSERT_TARGET, /* INSERT target list item */
EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */
EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */
+ EXPR_KIND_UPDATE_PORTION, /* UPDATE FOR PORTION OF item */
EXPR_KIND_GROUP_BY, /* GROUP BY */
EXPR_KIND_ORDER_BY, /* ORDER BY */
EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index a46ce1978e..2b77f82303 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -134,6 +134,7 @@ extern char get_rel_persistence(Oid relid);
extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes);
extern bool get_typisdefined(Oid typid);
+extern char *get_typname(Oid typid);
extern int16 get_typlen(Oid typid);
extern bool get_typbyval(Oid typid);
extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval);
diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h
index 204aee4054..b1eee65133 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -156,5 +156,8 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache);
extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1,
const RangeType *r2, RangeType **output1,
RangeType **output2);
+extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1,
+ const RangeType *r2, RangeType **output1,
+ RangeType **output2);
#endif /* RANGETYPES_H */
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
new file mode 100644
index 0000000000..66cc455428
--- /dev/null
+++ b/src/test/regress/expected/for_portion_of.out
@@ -0,0 +1,261 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+-- Fails on tables without a temporal PK:
+CREATE TABLE for_portion_of_test (
+ id int4range PRIMARY KEY,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL
+);
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity'
+SET name = 'foo';
+ERROR: relation "for_portion_of_test" does not have a temporal primary key
+LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity'
+ ^
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity';
+ERROR: relation "for_portion_of_test" does not have a temporal primary key
+LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity';
+ ^
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+--
+-- UPDATE tests
+--
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+SET name = 'foo'
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+ ^
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO 'Infinity'
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+ERROR: multiple assignments to same column "valid_at"
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 'Infinity' TO '-Infinity'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO 'Infinity'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO 'Infinity'
+SET name = 'four^2'
+WHERE id = '[4,5)';
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+SET name = 'two^2'
+WHERE id = '[2,3)';
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+SET name = 'one^2'
+WHERE id = '[1,2)';
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO 'Infinity'
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO 'Infinity'
+SET name = name || '*';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+----------
+ [1,2) | ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018") | one^2
+ [1,2) | ["Sat Feb 03 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | three
+ [3,4) | ["Fri Jun 01 00:00:00 2018","Tue Jan 01 00:00:00 2030") | three^1
+ [3,4) | ["Tue Jan 01 00:00:00 2030",) | three^1*
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | five^3
+ [5,6) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | five^3
+ [5,6) | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | five
+ [5,6) | ["Tue Jan 01 00:00:00 2030",) | five*
+ [6,7) | ["Thu Feb 15 00:00:00 2018","Sat Mar 03 00:00:00 2018") | one^2
+ [6,7) | ["Sat Mar 03 00:00:00 2018","Wed Apr 04 00:00:00 2018") | one^2
+(18 rows)
+
+--
+-- DELETE tests
+--
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+WHERE id = '[5,6)';
+ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist
+LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+ ^
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM 'Infinity' TO '-Infinity'
+WHERE id = '[3,4)';
+ERROR: range lower bound must be less than or equal to range upper bound
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO 'Infinity'
+WHERE id = '[3,4)';
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-02-08'
+WHERE id = '[1,2)';
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+WHERE id = '[6,7)';
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO 'Infinity';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+ id | valid_at | name
+-------+---------------------------------------------------------+---------
+ [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2
+ [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2
+ [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2
+ [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three
+ [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1
+ [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3
+ [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2
+ [5,6) | (,"Sun Jan 01 00:00:00 2017") | five
+ [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3
+ [5,6) | ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") | five
+(11 rows)
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+NOTICE: BEFORE UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of
+NOTICE: AFTER INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of
+NOTICE: AFTER UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025")
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+NOTICE: BEFORE DELETE of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
+NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of
+NOTICE: BEFORE INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of
+NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of
+NOTICE: AFTER INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of
+NOTICE: AFTER DELETE of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025")
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index d9ce961be2..7663d94dde 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -48,6 +48,7 @@ f_star|f
fast_emp4000|t
float4_tbl|f
float8_tbl|f
+for_portion_of_test|t
func_index_heap|t
hash_f8_heap|t
hash_i4_heap|t
@@ -173,6 +174,7 @@ quad_poly_tbl|t
radix_text_tbl|t
ramp|f
real_city|f
+referencing_period_test|t
reservations|f
road|t
shighway|t
@@ -215,6 +217,7 @@ trigger_parted_p1|t
trigger_parted_p1_1|t
varchar_tbl|f
view_base_table|t
+without_overlaps_test|t
-- restore normal output mode
\a\t
--
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644
index 0000000000..8c167ebb83
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,363 @@
+-- Tests for WITHOUT OVERLAPS.
+--
+-- test input parser
+--
+-- PK with no columns just WITHOUT OVERLAPS:
+CREATE TABLE without_overlaps_test (
+ valid_at tsrange,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: ...STRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OV...
+ ^
+-- PK with a range column that isn't there:
+CREATE TABLE without_overlaps_test (
+ id INTEGER,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist
+-- PK with a PERIOD that isn't there:
+-- PK with a non-range column:
+CREATE TABLE without_overlaps_test (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type
+-- PK with one column plus a range:
+CREATE TABLE without_overlaps_test (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+-- PK with two columns plus a range:
+CREATE TABLE without_overlaps_test2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+DROP TABLE without_overlaps_test2;
+-- PK with one column plus a PERIOD:
+-- PK with two columns plus a PERIOD:
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE without_overlaps_test2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE without_overlaps_test2 DROP CONSTRAINT without_overlaps2_pk;
+DROP TABLE without_overlaps_test2;
+DROP TYPE textrange2;
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+DROP TABLE without_overlaps_test;
+CREATE TABLE without_overlaps_test (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE without_overlaps_test
+ ADD CONSTRAINT without_overlaps_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk';
+ pg_get_constraintdef
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+DROP TABLE without_overlaps_test;
+CREATE TABLE without_overlaps_test (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE without_overlaps_test
+ ADD CONSTRAINT without_overlaps_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+--
+-- test pg_get_constraintdef
+--
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk';
+ pg_get_constraintdef
+---------------------------------------------
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+(1 row)
+
+--
+-- test PK inserts
+--
+-- okay:
+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));
+-- should fail:
+INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+ERROR: conflicting key value violates exclusion constraint "without_overlaps_pk"
+DETAIL: Key (id, valid_at)=([1,2), ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")) conflicts with existing key (id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018")).
+INSERT INTO without_overlaps_test VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+ERROR: null value in column "id" of relation "without_overlaps_test" violates not-null constraint
+DETAIL: Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")).
+INSERT INTO without_overlaps_test VALUES ('[3,3]', NULL);
+ERROR: null value in column "valid_at" of relation "without_overlaps_test" violates not-null constraint
+DETAIL: Failing row contains ([3,4), null).
+--
+-- test changing the PK's dependencies
+--
+CREATE TABLE without_overlaps_test2 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at DROP NOT NULL;
+ERROR: column "valid_at" is in a primary key
+ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+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 parser
+--
+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 updates CASCADE
+--
+--
+-- test FK parent updates SET NULL
+--
+--
+-- test FK parent updates SET DEFAULT
+--
+--
+-- 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 FK parent deletes CASCADE
+--
+--
+-- test FK parent deletes SET NULL
+--
+--
+-- test FK parent deletes SET DEFAULT
+--
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5355da7b01..dade7844bf 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -108,12 +108,12 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath
# NB: temp.sql does a reconnect which transiently uses 2 connections,
# so keep this parallel group to at most 19 tests
# ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml without_overlaps
# ----------
# Another group of parallel tests
# ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain without_overlaps for_portion_of
# event triggers cannot run concurrently with any test that runs DDL
test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 1f677f63af..c31e58137e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -20,6 +20,8 @@ test: enum
test: money
test: rangetypes
test: multirangetypes
+test: without_overlaps
+test: for_portion_of
test: pg_lsn
test: regproc
test: strings
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
new file mode 100644
index 0000000000..81bdb05b01
--- /dev/null
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -0,0 +1,239 @@
+-- Tests for UPDATE/DELETE FOR PORTION OF
+
+-- Fails on tables without a temporal PK:
+CREATE TABLE for_portion_of_test (
+ id int4range PRIMARY KEY,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL
+);
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity'
+SET name = 'foo';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-15' TO 'Infinity';
+
+DROP TABLE for_portion_of_test;
+CREATE TABLE for_portion_of_test (
+ id int4range NOT NULL,
+ valid_at tsrange NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+INSERT INTO for_portion_of_test
+VALUES
+('[1,2)', '[2018-01-02,2018-02-03)', 'one'),
+('[1,2)', '[2018-02-03,2018-03-03)', 'one'),
+('[1,2)', '[2018-03-03,2018-04-04)', 'one'),
+('[2,3)', '[2018-01-01,2018-01-05)', 'two'),
+('[3,4)', '[2018-01-01,)', 'three'),
+('[4,5)', '(,2018-04-01)', 'four'),
+('[5,6)', '(,)', 'five')
+;
+
+--
+-- UPDATE tests
+--
+
+-- Setting with a missing column fails
+UPDATE for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+SET name = 'foo'
+WHERE id = '[5,6)';
+
+-- Setting the range fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO 'Infinity'
+SET valid_at = '[1990-01-01,1999-01-01)'
+WHERE id = '[5,6)';
+
+-- Setting with timestamps reversed fails
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM 'Infinity' TO '-Infinity'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Setting with timestamps equal does nothing
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+SET name = 'three^0'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO 'Infinity'
+SET name = 'three^1'
+WHERE id = '[3,4)';
+
+-- Updating a finite/open portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-03-01'
+SET name = 'three^2'
+WHERE id = '[3,4)';
+
+-- Updating an open/finite portion with an open/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-02-01'
+SET name = 'four^1'
+WHERE id = '[4,5)';
+
+-- Updating an open/finite portion with a finite/open target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO 'Infinity'
+SET name = 'four^2'
+WHERE id = '[4,5)';
+
+-- Updating a finite/finite portion with an exact fit
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01'
+SET name = 'four^3'
+WHERE id = '[4,5)';
+
+-- Updating an enclosed span
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+SET name = 'two^2'
+WHERE id = '[2,3)';
+
+-- Updating an open/open portion with a finite/finite target
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01'
+SET name = 'five^2'
+WHERE id = '[5,6)';
+
+-- Updating an enclosed span with separate protruding spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01'
+SET name = 'five^3'
+WHERE id = '[5,6)';
+
+-- Updating multiple enclosed spans
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+SET name = 'one^2'
+WHERE id = '[1,2)';
+
+-- Updating the non-range part of the PK:
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-02-15' TO 'Infinity'
+SET id = '[6,7)'
+WHERE id = '[1,2)';
+
+-- UPDATE with no WHERE clause
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2030-01-01' TO 'Infinity'
+SET name = name || '*';
+
+-- TODO: UPDATE with generated columns too
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+--
+-- DELETE tests
+--
+
+-- Deleting with a missing column fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF invalid_at FROM '2018-06-01' TO 'Infinity'
+WHERE id = '[5,6)';
+
+-- Deleting with timestamps reversed fails
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01'
+WHERE id = '[3,4)';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM 'Infinity' TO '-Infinity'
+WHERE id = '[3,4)';
+
+-- Deleting with timestamps equal does nothing
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01'
+WHERE id = '[3,4)';
+
+-- Deleting with a closed/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01'
+WHERE id = '[5,6)';
+
+-- Deleting with a closed/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2018-04-01' TO 'Infinity'
+WHERE id = '[3,4)';
+
+-- Deleting with an open/closed target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO '2018-02-08'
+WHERE id = '[1,2)';
+
+-- Deleting with an open/open target
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '-Infinity' TO 'Infinity'
+WHERE id = '[6,7)';
+
+-- DELETE with no WHERE clause
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2025-01-01' TO 'Infinity';
+
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
+
+-- TODO: and UPDATE ... RETURNING ... returns only the updated values (not the inserted side values)
+
+-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows
+
+CREATE FUNCTION for_portion_of_trigger()
+RETURNS trigger
+AS
+$$
+BEGIN
+ RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at;
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_for_portion_of_before_insert
+ BEFORE INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_insert
+ AFTER INSERT ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_update
+ BEFORE UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_update
+ AFTER UPDATE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_before_delete
+ BEFORE DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+CREATE TRIGGER trg_for_portion_of_after_delete
+ AFTER DELETE ON for_portion_of_test
+ FOR EACH ROW
+ EXECUTE FUNCTION for_portion_of_trigger();
+
+UPDATE for_portion_of_test
+FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01'
+SET name = 'five^4'
+WHERE id = '[5,6)';
+
+DELETE FROM for_portion_of_test
+FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01'
+WHERE id = '[5,6)';
+
+-- test that we run triggers on the DELETEd row and the INSERTed rows
+SELECT * FROM for_portion_of_test ORDER BY id, valid_at;
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 0000000000..83824a55d3
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,369 @@
+-- Tests for WITHOUT OVERLAPS.
+
+--
+-- test input parser
+--
+
+-- PK with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE without_overlaps_test (
+ valid_at tsrange,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a range column that isn't there:
+
+CREATE TABLE without_overlaps_test (
+ id INTEGER,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with a PERIOD that isn't there:
+-- TODO
+
+-- PK with a non-range column:
+
+CREATE TABLE without_overlaps_test (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with one column plus a range:
+
+CREATE TABLE without_overlaps_test (
+ -- Since we can't depend on having btree_gist here,
+ -- use an int4range instead of an int.
+ -- (The rangetypes regression test uses the same trick.)
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- PK with two columns plus a range:
+CREATE TABLE without_overlaps_test2 (
+ id1 int4range,
+ id2 int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS)
+);
+DROP TABLE without_overlaps_test2;
+
+
+-- PK with one column plus a PERIOD:
+-- TODO
+
+-- PK with two columns plus a PERIOD:
+-- TODO
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE without_overlaps_test2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE without_overlaps_test2 DROP CONSTRAINT without_overlaps2_pk;
+DROP TABLE without_overlaps_test2;
+DROP TYPE textrange2;
+
+--
+-- test ALTER TABLE ADD CONSTRAINT
+--
+
+DROP TABLE without_overlaps_test;
+CREATE TABLE without_overlaps_test (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE without_overlaps_test
+ ADD CONSTRAINT without_overlaps_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk';
+
+DROP TABLE without_overlaps_test;
+CREATE TABLE without_overlaps_test (
+ id int4range,
+ valid_at tsrange
+);
+ALTER TABLE without_overlaps_test
+ ADD CONSTRAINT without_overlaps_pk
+ PRIMARY KEY (id, valid_at WITHOUT OVERLAPS);
+
+--
+-- test pg_get_constraintdef
+--
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk';
+
+--
+-- test PK inserts
+--
+
+-- okay:
+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));
+
+-- should fail:
+INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO without_overlaps_test VALUES (NULL, tsrange('2018-01-01', '2018-01-05'));
+INSERT INTO without_overlaps_test VALUES ('[3,3]', NULL);
+
+--
+-- test changing the PK's dependencies
+--
+
+CREATE TABLE without_overlaps_test2 (
+ id int4range,
+ valid_at tsrange,
+ CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+
+ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at DROP NOT NULL;
+ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at));
+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 parser
+--
+
+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 updates CASCADE
+--
+-- TODO
+--
+-- test FK parent updates SET NULL
+--
+-- TODO
+--
+-- test FK parent updates SET DEFAULT
+--
+-- TODO
+
+--
+-- 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 FK parent deletes CASCADE
+--
+-- TODO
+--
+-- test FK parent deletes SET NULL
+--
+-- TODO
+--
+-- test FK parent deletes SET DEFAULT
+--
+-- TODO