diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index fd6777ae01..7da5c76aef 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
+
@@ -922,7 +927,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)
@@ -955,14 +961,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.
+
@@ -1019,8 +1042,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 ]
@@ -1031,11 +1054,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 67144aa3c9..b0af555469 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2462,6 +2462,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 0974f3e23a..af1a1ec6b3 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1788,6 +1788,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
*/
@@ -1811,11 +1812,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());
@@ -1890,6 +1893,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 0d70cb0c3c..e64184b908 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;
@@ -185,6 +186,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 75552c64ed..a3a0bb100d 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -1114,6 +1114,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 a29c14bf1c..a17fd41a89 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -336,16 +336,19 @@ 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,
Oid *funcid);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
- Oid pkindOid, Oid constraintOid);
+ Oid pkindOid, Oid constraintOid, bool temporal);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode,
AlterTableUtilityContext *context);
@@ -450,12 +453,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);
@@ -472,6 +475,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,
@@ -5118,7 +5127,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
@@ -8353,6 +8363,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;
@@ -8455,6 +8470,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
@@ -8467,6 +8490,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid,
&fkconstraint->pk_attrs,
pkattnum, pktypoid,
+ &fkconstraint->pk_period,
+ &pkperiodattnum, &pkperiodtypoid,
opclasses);
}
else
@@ -8474,8 +8499,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);
}
@@ -8525,6 +8557,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.
@@ -8534,187 +8568,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;
}
/*
@@ -8730,7 +8604,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,
@@ -8743,6 +8618,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
+ is_temporal,
lockmode);
/*
@@ -8783,7 +8659,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;
@@ -8865,6 +8742,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
+ is_temporal,
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -8939,7 +8817,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);
@@ -8988,7 +8866,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));
@@ -9133,6 +9011,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
+ is_temporal,
false);
/*
@@ -9159,6 +9038,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
+ is_temporal,
lockmode);
table_close(partition, NoLock);
@@ -9342,7 +9222,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ constrForm->contemporal);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
@@ -9535,6 +9416,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 */
@@ -9564,11 +9446,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
@@ -10108,10 +10193,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.
*/
@@ -10119,6 +10206,8 @@ static int
transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
List **attnamelist,
int16 *attnums, Oid *atttypids,
+ Node **periodattname,
+ int16 *periodattnums, Oid *periodatttypids,
Oid *opclasses)
{
List *indexoidlist;
@@ -10186,35 +10275,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;
@@ -10241,6 +10345,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")));
}
/*
@@ -10262,12 +10370,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))
@@ -10307,6 +10419,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
@@ -10416,7 +10541,8 @@ validateForeignKeyConstraint(char *conname,
Relation rel,
Relation pkrel,
Oid pkindOid,
- Oid constraintOid)
+ Oid constraintOid,
+ bool temporal)
{
TupleTableSlot *slot;
TableScanDesc scan;
@@ -10446,8 +10572,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;
/*
@@ -10507,6 +10635,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
@@ -10518,7 +10647,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;
@@ -10526,12 +10658,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;
}
@@ -10577,37 +10715,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;
@@ -10633,37 +10812,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 092ac1646d..a02d63146d 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -724,6 +724,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 483bb65ddc..81f877827b 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3128,6 +3128,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 7179f589f9..a2d3603366 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1239,6 +1239,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 29e07b7228..55e623638e 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"
@@ -701,6 +702,127 @@ ExecInsert(ModifyTableState *mtstate,
return result;
}
+/*
+ * Insert tuples for the untouched timespan of a row in a FOR PORTION OF UPDATE/DELETE
+ * TODO: figure out if I need to make a copy of slot somehow in order to insert it
+ */
+static void
+ExecForPortionOfLeftovers(ModifyTableState *mtstate, EState *estate, ResultRelInfo *resultRelInfo, ItemPointer tupleid, TupleTableSlot *slot, TupleTableSlot *planSlot)
+{
+
+ 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;
+
+ /* 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
+ {
+ ExprState *exprState;
+
+ ExprContext *econtext = GetPerTupleExprContext(estate);
+ econtext->ecxt_scantuple = slot;
+
+ 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, mtstate->rootResultRelInfo, 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, mtstate->rootResultRelInfo, leftoverTuple2, planSlot,
+ estate, node->canSetTag);
+ }
+}
+
/* ----------------------------------------------------------------
* ExecDelete
*
@@ -742,6 +864,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;
@@ -1006,6 +1130,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);
@@ -1219,6 +1350,8 @@ ExecUpdate(ModifyTableState *mtstate,
TM_Result result;
TM_FailureData tmfd;
List *recheckIndexes = NIL;
+ ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
+ ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
/*
* abort the operation if not running transactions
@@ -1237,7 +1370,11 @@ ExecUpdate(ModifyTableState *mtstate,
return NULL; /* "do nothing" */
}
- /* INSTEAD OF ROW UPDATE Triggers */
+ /* 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)
{
@@ -1521,6 +1658,14 @@ 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,
@@ -2284,7 +2429,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);
@@ -2505,6 +2650,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
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 2b4d7654cc..b6a5383a2d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -214,6 +214,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);
@@ -2246,6 +2247,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
*
@@ -2586,6 +2609,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)
{
@@ -2972,12 +3008,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);
@@ -3523,6 +3562,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);
@@ -5142,6 +5182,9 @@ copyObjectImpl(const void *from)
case T_OnConflictExpr:
retval = _copyOnConflictExpr(from);
break;
+ case T_ForPortionOfExpr:
+ retval = _copyForPortionOfExpr(from);
+ break;
/*
* RELATION NODES
@@ -5670,6 +5713,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 e2d1b987bf..e4ea88e52b 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
*/
@@ -2830,6 +2847,17 @@ _equalOnConflictClause(const OnConflictClause *a, const OnConflictClause *b)
return true;
}
+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)
{
@@ -3206,6 +3234,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;
@@ -3724,6 +3755,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 08a049232e..2f646691f5 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -415,9 +415,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);
}
@@ -1707,6 +1709,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.
@@ -4002,6 +4021,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 ab7b535caa..89309f00fd 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1374,6 +1374,27 @@ _readAppendRelInfo(void)
* Stuff from parsenodes.h.
*/
+/*
+ * _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
*/
@@ -1646,6 +1667,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);
@@ -2754,6 +2776,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 94280a730c..96c327943d 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -297,7 +297,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);
@@ -2674,6 +2675,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);
@@ -6784,7 +6786,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;
@@ -6839,6 +6842,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 986d7a52e3..4a2888134d 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1794,6 +1794,7 @@ inheritance_planner(PlannerInfo *root)
returningLists,
rowMarks,
NULL,
+ parse->forPortionOf,
assign_special_exec_param(root)));
}
@@ -2378,6 +2379,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 5281a2f998..6aeeba01a8 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3538,7 +3538,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;
@@ -3612,6 +3612,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 c159fb2957..9f3dc64683 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,215 @@ 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;
+ FuncCall *fc;
+
+ 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)
+ {
+ Oid pkoid;
+ HeapTuple indexTuple;
+ Var *v;
+ Form_pg_index pk;
+ 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 */
+ 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?
+ indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(pkoid));
+ if (!HeapTupleIsValid(indexTuple)) /* should not happen */
+ elog(ERROR, "cache lookup failed for index %u", pkoid);
+
+ 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)));
+ }
+
+ 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;
+
+ 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)
+ {
+ TargetEntry *tle;
+
+ /* TODO: Maybe need a copy here?*/
+ Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*",
+ (Node *) result->range, (Node *) fc,
+ forPortionOf->range_name_location);
+
+ rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION);
+ 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
@@ -2230,6 +2460,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
{
Query *qry = makeNode(Query);
ParseNamespaceItem *nsitem;
+ Node *whereClause;
Node *qual;
qry->commandType = CMD_UPDATE;
@@ -2247,6 +2478,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 */
@@ -2263,7 +2498,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);
@@ -2272,7 +2516,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);
@@ -2290,7 +2534,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;
@@ -2341,6 +2585,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,
@@ -2357,6 +2604,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 480d168346..2418662f91 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -237,6 +237,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;
@@ -483,10 +484,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
@@ -506,6 +509,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
@@ -682,8 +686,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
@@ -3507,6 +3511,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;
@@ -3723,18 +3728,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;
@@ -3745,6 +3751,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;
@@ -3773,19 +3780,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);
@@ -3808,6 +3817,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);
@@ -10950,13 +10974,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;
}
@@ -11019,6 +11045,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
@@ -11026,10 +11053,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;
}
@@ -12186,6 +12214,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
*/
@@ -15242,6 +15284,7 @@ unreserved_keyword:
| PASSWORD
| PLANS
| POLICY
+ | PORTION
| PRECEDING
| PREPARE
| PREPARED
@@ -15523,6 +15566,7 @@ reserved_keyword:
| ONLY
| OR
| ORDER
+ | PERIOD
| PLACING
| PRIMARY
| REFERENCES
@@ -15811,9 +15855,11 @@ bare_label_keyword:
| PARTITION
| PASSING
| PASSWORD
+ | PERIOD
| PLACING
| PLANS
| POLICY
+ | PORTION
| POSITION
| PRECEDING
| PREPARE
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f5165863d7..11d075759c 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -3501,6 +3501,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 015b0538e3..3332b52cca 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1546,6 +1546,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->oldFirstRelfilenodeSubid = InvalidSubTransactionId;
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;
@@ -2088,7 +2089,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)
{
@@ -2106,6 +2107,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
*/
}
index->isconstraint = true;
+ index->istemporal = constraint->without_overlaps != NULL;
index->deferrable = constraint->deferrable;
index->initdeferred = constraint->initdeferred;
@@ -2486,6 +2488,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 1faaafab08..d12ebe9f12 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2988,6 +2988,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 01ad8bc240..2016b5ac91 100644
--- a/src/backend/utils/adt/rangetypes.c
+++ b/src/backend/utils/adt/rangetypes.c
@@ -2490,3 +2490,46 @@ datum_write(Pointer ptr, Datum datum, bool typbyval, char typalign,
return ptr;
}
+
+/*
+ * 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);
+ }
+}
+
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 7e2b2e3dd6..49f48f3f04 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 -
*
@@ -2059,6 +2212,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,
@@ -2654,9 +2808,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
@@ -2664,7 +2821,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;
@@ -2697,29 +2854,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 6c656586e8..97bfa896a4 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,
@@ -2009,6 +2009,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
{
Datum val;
bool isnull;
+ bool hasperiod;
const char *string;
/* Start off the constraint definition */
@@ -2021,7 +2022,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(",
@@ -2035,7 +2042,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, ')');
@@ -2136,7 +2143,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, ')');
@@ -2338,7 +2351,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;
@@ -2356,9 +2369,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 140339073b..e15078398e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2076,6 +2076,32 @@ get_typisdefined(Oid typid)
return false;
}
+/*
+ * get_typname
+ *
+ * Returns the name of a given type
+ *
+ * Returns a palloc'd copy of the string, or NULL if no such type.
+ */
+char *
+get_typname(Oid typid)
+{
+ HeapTuple tp;
+
+ tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+ if (HeapTupleIsValid(tp))
+ {
+ Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+ char *result;
+
+ result = pstrdup(NameStr(typtup->typname));
+ ReleaseSysCache(tp);
+ return result;
+ }
+ else
+ return NULL;
+}
+
/*
* get_typlen
*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9061af81a3..2e36fa7274 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4567,11 +4567,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;
@@ -5173,8 +5179,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.
*/
@@ -5240,7 +5247,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 ff45e3fb8c..a88f1395f4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7028,7 +7028,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++)
@@ -7089,7 +7090,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) "
@@ -7128,7 +7130,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 "
@@ -7163,7 +7166,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 "
@@ -7194,7 +7198,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 "
@@ -7228,7 +7233,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 "
@@ -7268,6 +7274,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));
@@ -7331,6 +7338,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;
}
@@ -16663,9 +16671,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 e0b42e8391..0fe82ab8aa 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -450,6 +450,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 ec63662060..86b2e19ec5 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -638,6 +638,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 07d640021c..5063833e28 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2391,6 +2391,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 f58e8675f3..6cc5857565 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 bbcac69d48..76a8e0fd75 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3768,6 +3768,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 6c0a7d68d6..599be9ab1d 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 ParallelHashJoinState;
@@ -387,6 +389,22 @@ typedef struct OnConflictSetState
ExprState *oc_WhereClause; /* state for the WHERE clause */
} 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
*
@@ -476,6 +494,9 @@ typedef struct ResultRelInfo
/* ON CONFLICT evaluation state */
OnConflictSetState *ri_onConflict;
+ /* FOR PORTION OF evaluation state */
+ ForPortionOfState *ri_forPortionOf;
+
/* partition check expression state (NULL if not set up yet) */
ExprState *ri_PartitionCheckExpr;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 7ddd8c011b..ac53f29f58 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,
@@ -197,6 +198,7 @@ typedef enum NodeTag
T_JoinExpr,
T_FromExpr,
T_OnConflictExpr,
+ T_ForPortionOfExpr,
T_IntoClause,
/*
@@ -471,6 +473,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 60c2f45466..ea97893df9 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 */
@@ -1386,6 +1388,19 @@ typedef struct RowMarkClause
bool pushedDown; /* pushed down from higher query level? */
} 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
@@ -1541,12 +1556,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;
/* ----------------------
@@ -1555,13 +1571,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;
/* ----------------------
@@ -2187,7 +2204,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 */
@@ -2195,6 +2214,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? */
@@ -2793,6 +2815,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 3dd16b9ad5..d39b63802f 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1821,6 +1821,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 7e6b10f86b..951771b4e8 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -226,6 +226,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 fd65ee8f9c..b2d9abe3c4 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1541,4 +1541,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 715a24ad29..cbdb984136 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -264,7 +264,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 71dcdf2889..4915e858f4 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -306,9 +306,11 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 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 fecfe1f4f6..1990732119 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -137,6 +137,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 b77c41cf1b..b60f0863be 100644
--- a/src/include/utils/rangetypes.h
+++ b/src/include/utils/rangetypes.h
@@ -135,5 +135,8 @@ extern int range_cmp_bound_values(TypeCacheEntry *typcache, const RangeBound *b1
extern bool bounds_adjacent(TypeCacheEntry *typcache, RangeBound bound1,
RangeBound bound2);
extern RangeType *make_empty_range(TypeCacheEntry *typcache);
+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/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 192445878d..2b71f53c5f 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
@@ -172,6 +173,7 @@ quad_poly_tbl|t
radix_text_tbl|t
ramp|f
real_city|f
+referencing_period_test|t
road|t
shighway|t
slow_emp4000|f
@@ -213,6 +215,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/parallel_schedule b/src/test/regress/parallel_schedule
index ae89ed7f0b..6c71eeeae4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,12 +107,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 525bdc804f..0caf585e64 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -21,6 +21,8 @@ test: enum
test: money
test: rangetypes
test: pg_lsn
+test: without_overlaps
+test: for_portion_of
test: regproc
test: strings
test: numerology