diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm index 368b1dea3e..f6cfbb68f6 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 b7bcdd9d0f..c1346d765c 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2399,6 +2399,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 99ae159f98..41645ec692 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1832,6 +1832,7 @@ index_constraint_create(Relation heapRelation, islocal, inhcount, noinherit, + false, /* contemporal */ is_internal); /* diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index b6145593a3..e913741103 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -76,6 +76,7 @@ CreateConstraintEntry(const char *constraintName, bool conIsLocal, int conInhCount, bool conNoInherit, + bool conTemporal, bool is_internal) { Relation conDesc; @@ -184,6 +185,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/tablecmds.c b/src/backend/commands/tablecmds.c index fb2be10794..217d7f385c 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -328,9 +328,12 @@ static int transformColumnNameList(Oid relId, List *colList, static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses); static Oid transformFkeyCheckAttrs(Relation pkrel, int numattrs, int16 *attnums, + bool is_temporal, int16 *periodattnums, Oid *opclasses); static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts); static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId, @@ -338,7 +341,7 @@ static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId, static void validateCheckConstraint(Relation rel, HeapTuple constrtup); static void validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, - Oid pkindOid, Oid constraintOid); + Oid pkindOid, Oid constraintOid, bool temporal); static void ATController(AlterTableStmt *parsetree, Relation rel, List *cmds, bool recurse, LOCKMODE lockmode); static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, @@ -424,12 +427,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); @@ -446,6 +449,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, @@ -4709,7 +4718,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 @@ -7604,6 +7614,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[1]; + int16 fkperiodattnum[1]; + Oid pkperiodtypoid[1]; + Oid fkperiodtypoid[1]; int i; int numfks, numpks; @@ -7706,6 +7721,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numfks = transformColumnNameList(RelationGetRelid(rel), fkconstraint->fk_attrs, fkattnum, fktypoid); + if (is_temporal) + { + List *fk_period; + MemSet(pkperiodattnum, 0, sizeof(pkperiodattnum)); + MemSet(fkperiodattnum, 0, sizeof(fkperiodattnum)); + MemSet(pkperiodtypoid, 0, sizeof(pkperiodtypoid)); + MemSet(fkperiodtypoid, 0, sizeof(fkperiodtypoid)); + 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 @@ -7718,6 +7745,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid, &fkconstraint->pk_attrs, pkattnum, pktypoid, + &fkconstraint->pk_period, + pkperiodattnum, pkperiodtypoid, opclasses); } else @@ -7725,8 +7754,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); } @@ -7776,6 +7812,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. @@ -7785,188 +7823,29 @@ 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)); - } - - pfeqoperators[i] = pfeqop; - ppeqoperators[i] = ppeqop; - ffeqoperators[i] = ffeqop; + 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[0]; + fkattnum[numpks] = fkperiodattnum[0]; + pktypoid[numpks] = pkperiodtypoid[0]; + fktypoid[numpks] = fkperiodtypoid[0]; + + FindFKComparisonOperators( + fkconstraint, tab, numpks, fkattnum, + &old_check_ok, &old_pfeqop_item, + pkperiodtypoid[0], fkperiodtypoid[0], opclasses[numpks], + is_temporal, true, + &pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]); + numfks += 1; + numpks += 1; + } /* * Create all the constraint and trigger objects, recursing to partitions @@ -7981,7 +7860,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, @@ -7994,6 +7874,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, lockmode); /* @@ -8034,7 +7915,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; @@ -8110,12 +7992,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, fkconstraint->fk_upd_action, fkconstraint->fk_del_action, fkconstraint->fk_matchtype, - NULL, /* no exclusion constraint */ + NULL, NULL, /* no check constraint */ NULL, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ + is_temporal, false); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); @@ -8191,7 +8074,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); @@ -8240,7 +8123,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)); @@ -8386,6 +8269,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, false, 1, false, + is_temporal, false); /* @@ -8412,6 +8296,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, lockmode); table_close(partition, NoLock); @@ -8544,6 +8429,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop); + for (int i = 0; i < numfks; i++) mapped_confkey[i] = attmap[confkey[i] - 1]; @@ -8589,7 +8475,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop, - true); + true, + constrForm->contemporal); table_close(fkRel, NoLock); ReleaseSysCache(tuple); @@ -8783,6 +8670,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 */ @@ -8812,11 +8700,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 @@ -9197,7 +9288,7 @@ ATExecValidateConstraint(Relation rel, char *constrName, bool recurse, validateForeignKeyConstraint(constrName, rel, refrel, con->conindid, - con->oid); + con->oid, con->contemporal); table_close(refrel, NoLock); /* @@ -9330,10 +9421,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. */ @@ -9341,6 +9434,8 @@ static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses) { List *indexoidlist; @@ -9408,35 +9503,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[i] = pkattno; + periodatttypids[i] = 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; @@ -9463,6 +9573,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"))); } /* @@ -9484,12 +9598,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)) @@ -9529,6 +9647,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 @@ -9718,7 +9849,8 @@ validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, - Oid constraintOid) + Oid constraintOid, + bool temporal) { TupleTableSlot *slot; TableScanDesc scan; @@ -9748,8 +9880,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; /* @@ -9811,6 +9945,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 @@ -9822,7 +9957,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; @@ -9830,12 +9968,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; } @@ -9881,6 +10025,13 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr fk_trigger->whenClause = NULL; fk_trigger->isconstraint = true; fk_trigger->constrrel = NULL; + + if ((fkconstraint->fk_period != NULL) && + ((fkconstraint->fk_del_action != FKCONSTR_ACTION_NOACTION) || + (fkconstraint->fk_del_action != FKCONSTR_ACTION_RESTRICT))) + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_del_action); + switch (fkconstraint->fk_del_action) { case FKCONSTR_ACTION_NOACTION: @@ -9910,7 +10061,7 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr break; default: elog(ERROR, "unrecognized FK action type: %d", - (int) fkconstraint->fk_del_action); + (int) fkconstraint->fk_del_action); break; } fk_trigger->args = NIL; @@ -9937,37 +10088,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 2d9a8e9d54..2ff3d8b81b 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -776,6 +776,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 89887b8fd7..04f92fee95 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3174,6 +3174,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/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 6414aded0e..15705b7653 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2921,12 +2921,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); diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index a9b2f8bacd..33b7b0866c 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1962,7 +1962,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) { @@ -2351,6 +2351,148 @@ 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 = 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. + */ + } + 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/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 8c895459c3..9cf3c99d78 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -109,6 +109,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 */ @@ -192,7 +193,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); @@ -353,18 +354,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, true, true) 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[1] + * 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, true, true) 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++) { @@ -382,6 +411,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[1]", riinfo->nkeys); /* Prepare and save the plan */ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids, @@ -1176,7 +1207,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. */ @@ -1269,13 +1300,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 - * @@ -2051,6 +2204,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, @@ -2649,9 +2803,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 @@ -2659,7 +2816,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; @@ -2692,29 +2849,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 0c58f1f109..473b0d1aba 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -317,7 +317,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, @@ -1985,6 +1985,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, { Datum val; bool isnull; + bool hasperiod; const char *string; /* Start off the constraint definition */ @@ -1997,7 +1998,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(", @@ -2011,7 +2018,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, ')'); @@ -2112,7 +2119,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, ')'); @@ -2314,7 +2327,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; @@ -2332,9 +2345,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/relcache.c b/src/backend/utils/cache/relcache.c index 7aa5d7c7fa..4d863d0715 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4949,8 +4949,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. */ @@ -5016,7 +5017,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/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index c1e60c7dfd..0baa22c0e5 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -103,6 +103,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 */ /* @@ -117,26 +123,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]; @@ -211,6 +217,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 0902dce5f1..1bca4e417e 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3703,6 +3703,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' },