From 420a90c72183e2e468698faca0e72d30339f07e0 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 1 Dec 2025 16:26:39 -0600 Subject: [PATCH v1 1/1] move partition code in tablecmds.c to new file --- src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/partcmds.c | 3377 ++++++++++++++++++++++++ src/backend/commands/tablecmds.c | 3456 +------------------------ src/backend/partitioning/partbounds.c | 1 + src/include/commands/partcmds.h | 53 + src/include/commands/tablecmds.h | 134 +- 7 files changed, 3575 insertions(+), 3448 deletions(-) create mode 100644 src/backend/commands/partcmds.c create mode 100644 src/include/commands/partcmds.h diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 64cb6278409..6ffb3fed2be 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -45,6 +45,7 @@ OBJS = \ matview.o \ opclasscmds.o \ operatorcmds.o \ + partcmds.o \ policy.o \ portalcmds.o \ prepare.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 5fc35826b1c..155e9e294dc 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -33,6 +33,7 @@ backend_sources += files( 'matview.c', 'opclasscmds.c', 'operatorcmds.c', + 'partcmds.c', 'policy.c', 'portalcmds.c', 'prepare.c', diff --git a/src/backend/commands/partcmds.c b/src/backend/commands/partcmds.c new file mode 100644 index 00000000000..0f72f698df5 --- /dev/null +++ b/src/backend/commands/partcmds.c @@ -0,0 +1,3377 @@ +/*------------------------------------------------------------------------- + * + * partcmds.c + * TODO + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/partcmds.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/htup_details.h" +#include "utils/lsyscache.h" +#include "access/relation.h" +#include "access/skey.h" +#include "access/stratnum.h" +#include "access/table.h" +#include "catalog/heap.h" +#include "catalog/index.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/partition.h" +#include "catalog/pg_am_d.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_inherits.h" +#include "catalog/pg_trigger.h" +#include "commands/defrem.h" +#include "commands/partcmds.h" +#include "commands/tablecmds.h" +#include "commands/trigger.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_node.h" +#include "parser/parse_relation.h" +#include "parser/parse_utilcmd.h" +#include "partitioning/partbounds.h" +#include "partitioning/partdesc.h" +#include "storage/lmgr.h" +#include "tcop/utility.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/inval.h" +#include "utils/memutils.h" +#include "utils/partcache.h" +#include "utils/rel.h" +#include "utils/syscache.h" + +/* + * RemoveInheritedConstraint + * + * Removes the constraint and its associated trigger from the specified + * relation, which inherited the given constraint. + */ +static void +RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid, + Oid conrelid) +{ + ObjectAddresses *objs; + HeapTuple consttup; + ScanKeyData key; + SysScanDesc scan; + HeapTuple trigtup; + + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conrelid)); + + scan = systable_beginscan(conrel, + ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + objs = new_object_addresses(); + while ((consttup = systable_getnext(scan)) != NULL) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup); + + if (conform->conparentid != conoid) + continue; + else + { + ObjectAddress addr; + SysScanDesc scan2; + ScanKeyData key2; + int n PG_USED_FOR_ASSERTS_ONLY; + + ObjectAddressSet(addr, ConstraintRelationId, conform->oid); + add_exact_object_address(&addr, objs); + + /* + * First we must delete the dependency record that binds the + * constraint records together. + */ + n = deleteDependencyRecordsForSpecific(ConstraintRelationId, + conform->oid, + DEPENDENCY_INTERNAL, + ConstraintRelationId, + conoid); + Assert(n == 1); /* actually only one is expected */ + + /* + * Now search for the triggers for this constraint and set them up + * for deletion too + */ + ScanKeyInit(&key2, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conform->oid)); + scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId, + true, NULL, 1, &key2); + while ((trigtup = systable_getnext(scan2)) != NULL) + { + ObjectAddressSet(addr, TriggerRelationId, + ((Form_pg_trigger) GETSTRUCT(trigtup))->oid); + add_exact_object_address(&addr, objs); + } + systable_endscan(scan2); + } + } + /* make the dependency deletions visible */ + CommandCounterIncrement(); + performMultipleDeletions(objs, DROP_RESTRICT, + PERFORM_DELETION_INTERNAL); + systable_endscan(scan); +} + +/* + * DropForeignKeyConstraintTriggers + * + * The subroutine for tryAttachPartitionForeignKey handles the deletion of + * action triggers for the foreign key constraint. + * + * If valid confrelid and conrelid values are not provided, the respective + * trigger check will be skipped, and the trigger will be considered for + * removal. + */ +static void +DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid, + Oid conrelid) +{ + ScanKeyData key; + SysScanDesc scan; + HeapTuple trigtup; + + ScanKeyInit(&key, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true, + NULL, 1, &key); + while ((trigtup = systable_getnext(scan)) != NULL) + { + Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup); + ObjectAddress trigger; + + /* Invalid if trigger is not for a referential integrity constraint */ + if (!OidIsValid(trgform->tgconstrrelid)) + continue; + if (OidIsValid(conrelid) && trgform->tgconstrrelid != conrelid) + continue; + if (OidIsValid(confrelid) && trgform->tgrelid != confrelid) + continue; + + /* We should be dropping trigger related to foreign key constraint */ + Assert(trgform->tgfoid == F_RI_FKEY_CHECK_INS || + trgform->tgfoid == F_RI_FKEY_CHECK_UPD || + trgform->tgfoid == F_RI_FKEY_CASCADE_DEL || + trgform->tgfoid == F_RI_FKEY_CASCADE_UPD || + trgform->tgfoid == F_RI_FKEY_RESTRICT_DEL || + trgform->tgfoid == F_RI_FKEY_RESTRICT_UPD || + trgform->tgfoid == F_RI_FKEY_SETNULL_DEL || + trgform->tgfoid == F_RI_FKEY_SETNULL_UPD || + trgform->tgfoid == F_RI_FKEY_SETDEFAULT_DEL || + trgform->tgfoid == F_RI_FKEY_SETDEFAULT_UPD || + trgform->tgfoid == F_RI_FKEY_NOACTION_DEL || + trgform->tgfoid == F_RI_FKEY_NOACTION_UPD); + + /* + * The constraint is originally set up to contain this trigger as an + * implementation object, so there's a dependency record that links + * the two; however, since the trigger is no longer needed, we remove + * the dependency link in order to be able to drop the trigger while + * keeping the constraint intact. + */ + deleteDependencyRecordsFor(TriggerRelationId, + trgform->oid, + false); + /* make dependency deletion visible to performDeletion */ + CommandCounterIncrement(); + ObjectAddressSet(trigger, TriggerRelationId, + trgform->oid); + performDeletion(&trigger, DROP_RESTRICT, 0); + /* make trigger drop visible, in case the loop iterates */ + CommandCounterIncrement(); + } + + systable_endscan(scan); +} + +/* + * GetForeignKeyCheckTriggers + * Returns insert and update "check" triggers of the given relation + * belonging to the given constraint + */ +static void +GetForeignKeyCheckTriggers(Relation trigrel, + Oid conoid, Oid confrelid, Oid conrelid, + Oid *insertTriggerOid, + Oid *updateTriggerOid) +{ + ScanKeyData key; + SysScanDesc scan; + HeapTuple trigtup; + + *insertTriggerOid = *updateTriggerOid = InvalidOid; + ScanKeyInit(&key, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + + scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true, + NULL, 1, &key); + while ((trigtup = systable_getnext(scan)) != NULL) + { + Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup); + + if (trgform->tgconstrrelid != confrelid) + continue; + if (trgform->tgrelid != conrelid) + continue; + /* Only ever look at "check" triggers on the FK side. */ + if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_FK) + continue; + if (TRIGGER_FOR_INSERT(trgform->tgtype)) + { + Assert(*insertTriggerOid == InvalidOid); + *insertTriggerOid = trgform->oid; + } + else if (TRIGGER_FOR_UPDATE(trgform->tgtype)) + { + Assert(*updateTriggerOid == InvalidOid); + *updateTriggerOid = trgform->oid; + } +#ifndef USE_ASSERT_CHECKING + /* In an assert-enabled build, continue looking to find duplicates. */ + if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid)) + break; +#endif + } + + if (!OidIsValid(*insertTriggerOid)) + elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u", + conoid); + if (!OidIsValid(*updateTriggerOid)) + elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u", + conoid); + + systable_endscan(scan); +} + +/* + * AttachPartitionForeignKey + * + * The subroutine for tryAttachPartitionForeignKey performs the final tasks of + * attaching the constraint, removing redundant triggers and entries from + * pg_constraint, and setting the constraint's parent. + */ +static void +AttachPartitionForeignKey(List **wqueue, + Relation partition, + Oid partConstrOid, + Oid parentConstrOid, + Oid parentInsTrigger, + Oid parentUpdTrigger, + Relation trigrel) +{ + HeapTuple parentConstrTup; + Form_pg_constraint parentConstr; + HeapTuple partcontup; + Form_pg_constraint partConstr; + bool queueValidation; + Oid partConstrFrelid; + Oid partConstrRelid; + bool parentConstrIsEnforced; + + /* Fetch the parent constraint tuple */ + parentConstrTup = SearchSysCache1(CONSTROID, + ObjectIdGetDatum(parentConstrOid)); + if (!HeapTupleIsValid(parentConstrTup)) + elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid); + parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup); + parentConstrIsEnforced = parentConstr->conenforced; + + /* Fetch the child constraint tuple */ + partcontup = SearchSysCache1(CONSTROID, + ObjectIdGetDatum(partConstrOid)); + if (!HeapTupleIsValid(partcontup)) + elog(ERROR, "cache lookup failed for constraint %u", partConstrOid); + partConstr = (Form_pg_constraint) GETSTRUCT(partcontup); + partConstrFrelid = partConstr->confrelid; + partConstrRelid = partConstr->conrelid; + + /* + * If the referenced table is partitioned, then the partition we're + * attaching now has extra pg_constraint rows and action triggers that are + * no longer needed. Remove those. + */ + if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE) + { + Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock); + + RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid, + partConstrRelid); + + table_close(pg_constraint, RowShareLock); + } + + /* + * Will we need to validate this constraint? A valid parent constraint + * implies that all child constraints have been validated, so if this one + * isn't, we must trigger phase 3 validation. + */ + queueValidation = parentConstr->convalidated && !partConstr->convalidated; + + ReleaseSysCache(partcontup); + ReleaseSysCache(parentConstrTup); + + /* + * The action triggers in the new partition become redundant -- the parent + * table already has equivalent ones, and those will be able to reach the + * partition. Remove the ones in the partition. We identify them because + * they have our constraint OID, as well as being on the referenced rel. + */ + DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid, + partConstrRelid); + + ConstraintSetParentConstraint(partConstrOid, parentConstrOid, + RelationGetRelid(partition)); + + /* + * Like the constraint, attach partition's "check" triggers to the + * corresponding parent triggers if the constraint is ENFORCED. NOT + * ENFORCED constraints do not have these triggers. + */ + if (parentConstrIsEnforced) + { + Oid insertTriggerOid, + updateTriggerOid; + + GetForeignKeyCheckTriggers(trigrel, + partConstrOid, partConstrFrelid, partConstrRelid, + &insertTriggerOid, &updateTriggerOid); + Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger)); + TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger, + RelationGetRelid(partition)); + Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger)); + TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger, + RelationGetRelid(partition)); + } + + /* + * We updated this pg_constraint row above to set its parent; validating + * it will cause its convalidated flag to change, so we need CCI here. In + * addition, we need it unconditionally for the rare case where the parent + * table has *two* identical constraints; when reaching this function for + * the second one, we must have made our changes visible, otherwise we + * would try to attach both to this one. + */ + CommandCounterIncrement(); + + /* If validation is needed, put it in the queue now. */ + if (queueValidation) + { + Relation conrel; + Oid confrelid; + + conrel = table_open(ConstraintRelationId, RowExclusiveLock); + + partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid)); + if (!HeapTupleIsValid(partcontup)) + elog(ERROR, "cache lookup failed for constraint %u", partConstrOid); + + confrelid = ((Form_pg_constraint) GETSTRUCT(partcontup))->confrelid; + + /* Use the same lock as for AT_ValidateConstraint */ + QueueFKConstraintValidation(wqueue, conrel, partition, confrelid, + partcontup, ShareUpdateExclusiveLock); + ReleaseSysCache(partcontup); + table_close(conrel, RowExclusiveLock); + } +} + +/* + * 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 + * partition might already have an equivalent foreign key; this routine + * compares the given ForeignKeyCacheInfo (in the partition) to the FK defined + * by the other parameters. If they are equivalent, create the link between + * the two constraints and return true. + * + * If the given FK does not match the one defined by rest of the params, + * return false. + */ +bool +tryAttachPartitionForeignKey(List **wqueue, + ForeignKeyCacheInfo *fk, + Relation partition, + Oid parentConstrOid, + int numfks, + AttrNumber *mapped_conkey, + AttrNumber *confkey, + Oid *conpfeqop, + Oid parentInsTrigger, + Oid parentUpdTrigger, + Relation trigrel) +{ + HeapTuple parentConstrTup; + Form_pg_constraint parentConstr; + HeapTuple partcontup; + Form_pg_constraint partConstr; + + parentConstrTup = SearchSysCache1(CONSTROID, + ObjectIdGetDatum(parentConstrOid)); + if (!HeapTupleIsValid(parentConstrTup)) + elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid); + parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup); + + /* + * Do some quick & easy initial checks. If any of these fail, we cannot + * use this constraint. + */ + if (fk->confrelid != parentConstr->confrelid || fk->nkeys != numfks) + { + ReleaseSysCache(parentConstrTup); + return false; + } + for (int i = 0; i < numfks; i++) + { + if (fk->conkey[i] != mapped_conkey[i] || + fk->confkey[i] != confkey[i] || + fk->conpfeqop[i] != conpfeqop[i]) + { + ReleaseSysCache(parentConstrTup); + return false; + } + } + + /* Looks good so far; perform more extensive checks. */ + partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid)); + if (!HeapTupleIsValid(partcontup)) + elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); + partConstr = (Form_pg_constraint) GETSTRUCT(partcontup); + + /* + * An error should be raised if the constraint enforceability is + * different. Returning false without raising an error, as we do for other + * attributes, could lead to a duplicate constraint with the same + * enforceability as the parent. While this may be acceptable, it may not + * be ideal. Therefore, it's better to raise an error and allow the user + * to correct the enforceability before proceeding. + */ + if (partConstr->conenforced != parentConstr->conenforced) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"", + NameStr(parentConstr->conname), + NameStr(partConstr->conname), + RelationGetRelationName(partition)))); + + if (OidIsValid(partConstr->conparentid) || + partConstr->condeferrable != parentConstr->condeferrable || + partConstr->condeferred != parentConstr->condeferred || + partConstr->confupdtype != parentConstr->confupdtype || + partConstr->confdeltype != parentConstr->confdeltype || + partConstr->confmatchtype != parentConstr->confmatchtype) + { + ReleaseSysCache(parentConstrTup); + ReleaseSysCache(partcontup); + return false; + } + + ReleaseSysCache(parentConstrTup); + ReleaseSysCache(partcontup); + + /* Looks good! Attach this constraint. */ + AttachPartitionForeignKey(wqueue, partition, fk->conoid, + parentConstrOid, parentInsTrigger, + parentUpdTrigger, trigrel); + + return true; +} + +/* + * CloneFkReferencing + * Subroutine for CloneForeignKeyConstraints + * + * For each FK constraint of the parent relation in the given list, find an + * equivalent constraint in its partition relation that can be reparented; + * if one cannot be found, create a new constraint in the partition as its + * child. + * + * If wqueue is given, it is used to set up phase-3 verification for each + * cloned constraint; omit it if such verification is not needed + * (example: the partition is being created anew). + */ +static void +CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) +{ + AttrMap *attmap; + List *partFKs; + List *clone = NIL; + ListCell *cell; + Relation trigrel; + + /* obtain a list of constraints that we need to clone */ + foreach(cell, RelationGetFKeyList(parentRel)) + { + ForeignKeyCacheInfo *fk = lfirst(cell); + + /* + * Refuse to attach a table as partition that this partitioned table + * already has a foreign key to. This isn't useful schema, which is + * proven by the fact that there have been no user complaints that + * it's already impossible to achieve this in the opposite direction, + * i.e., creating a foreign key that references a partition. This + * restriction allows us to dodge some complexities around + * pg_constraint and pg_trigger row creations that would be needed + * during ATTACH/DETACH for this kind of relationship. + */ + if (fk->confrelid == RelationGetRelid(partRel)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot attach table \"%s\" as a partition because it is referenced by foreign key \"%s\"", + RelationGetRelationName(partRel), + get_constraint_name(fk->conoid)))); + + clone = lappend_oid(clone, fk->conoid); + } + + /* + * Silently do nothing if there's nothing to do. In particular, this + * avoids throwing a spurious error for foreign tables. + */ + if (clone == NIL) + return; + + if (partRel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("foreign key constraints are not supported on foreign tables"))); + + /* + * Triggers of the foreign keys will be manipulated a bunch of times in + * the loop below. To avoid repeatedly opening/closing the trigger + * catalog relation, we open it here and pass it to the subroutines called + * below. + */ + trigrel = table_open(TriggerRelationId, RowExclusiveLock); + + /* + * The constraint key may differ, if the columns in the partition are + * different. This map is used to convert them. + */ + attmap = build_attrmap_by_name(RelationGetDescr(partRel), + RelationGetDescr(parentRel), + false); + + partFKs = copyObject(RelationGetFKeyList(partRel)); + + foreach(cell, clone) + { + Oid parentConstrOid = lfirst_oid(cell); + Form_pg_constraint constrForm; + Relation pkrel; + HeapTuple tuple; + int numfks; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber mapped_conkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + Oid conpfeqop[INDEX_MAX_KEYS]; + Oid conppeqop[INDEX_MAX_KEYS]; + Oid conffeqop[INDEX_MAX_KEYS]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + Constraint *fkconstraint; + bool attached; + Oid indexOid; + ObjectAddress address; + ListCell *lc; + Oid insertTriggerOid = InvalidOid, + updateTriggerOid = InvalidOid; + bool with_period; + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", + parentConstrOid); + constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + /* Don't clone constraints whose parents are being cloned */ + if (list_member_oid(clone, constrForm->conparentid)) + { + ReleaseSysCache(tuple); + continue; + } + + /* + * Need to prevent concurrent deletions. If pkrel is a partitioned + * relation, that means to lock all partitions. + */ + pkrel = table_open(constrForm->confrelid, ShareRowExclusiveLock); + if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + (void) find_all_inheritors(RelationGetRelid(pkrel), + ShareRowExclusiveLock, NULL); + + DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey, + conpfeqop, conppeqop, conffeqop, + &numfkdelsetcols, confdelsetcols); + for (int i = 0; i < numfks; i++) + mapped_conkey[i] = attmap->attnums[conkey[i] - 1]; + + /* + * Get the "check" triggers belonging to the constraint, if it is + * ENFORCED, to pass as parent OIDs for similar triggers that will be + * created on the partition in addFkRecurseReferencing(). They are + * also passed to tryAttachPartitionForeignKey() below to simply + * assign as parents to the partition's existing "check" triggers, + * that is, if the corresponding constraints is deemed attachable to + * the parent constraint. + */ + if (constrForm->conenforced) + GetForeignKeyCheckTriggers(trigrel, constrForm->oid, + constrForm->confrelid, constrForm->conrelid, + &insertTriggerOid, &updateTriggerOid); + + /* + * Before creating a new constraint, see whether any existing FKs are + * fit for the purpose. If one is, attach the parent constraint to + * it, and don't clone anything. This way we avoid the expensive + * verification step and don't end up with a duplicate FK, and we + * don't need to recurse to partitions for this constraint. + */ + attached = false; + foreach(lc, partFKs) + { + ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc); + + if (tryAttachPartitionForeignKey(wqueue, + fk, + partRel, + parentConstrOid, + numfks, + mapped_conkey, + confkey, + conpfeqop, + insertTriggerOid, + updateTriggerOid, + trigrel)) + { + attached = true; + table_close(pkrel, NoLock); + break; + } + } + if (attached) + { + ReleaseSysCache(tuple); + continue; + } + + /* No dice. Set up to create our own constraint */ + fkconstraint = makeNode(Constraint); + fkconstraint->contype = CONSTRAINT_FOREIGN; + /* ->conname determined below */ + fkconstraint->deferrable = constrForm->condeferrable; + fkconstraint->initdeferred = constrForm->condeferred; + fkconstraint->location = -1; + fkconstraint->pktable = NULL; + /* ->fk_attrs determined below */ + fkconstraint->pk_attrs = NIL; + fkconstraint->fk_matchtype = constrForm->confmatchtype; + fkconstraint->fk_upd_action = constrForm->confupdtype; + fkconstraint->fk_del_action = constrForm->confdeltype; + fkconstraint->fk_del_set_cols = NIL; + fkconstraint->old_conpfeqop = NIL; + fkconstraint->old_pktable_oid = InvalidOid; + fkconstraint->is_enforced = constrForm->conenforced; + fkconstraint->skip_validation = false; + fkconstraint->initially_valid = constrForm->convalidated; + for (int i = 0; i < numfks; i++) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(partRel), + mapped_conkey[i] - 1); + fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, + makeString(NameStr(att->attname))); + } + + indexOid = constrForm->conindid; + with_period = constrForm->conperiod; + + /* Create the pg_constraint entry at this level */ + address = addFkConstraint(addFkReferencingSide, + NameStr(constrForm->conname), fkconstraint, + partRel, pkrel, indexOid, parentConstrOid, + numfks, confkey, + mapped_conkey, conpfeqop, + conppeqop, conffeqop, + numfkdelsetcols, confdelsetcols, + false, with_period); + + /* Done with the cloned constraint's tuple */ + ReleaseSysCache(tuple); + + /* Create the check triggers, and recurse to partitions, if any */ + addFkRecurseReferencing(wqueue, + fkconstraint, + partRel, + pkrel, + indexOid, + address.objectId, + numfks, + confkey, + mapped_conkey, + conpfeqop, + conppeqop, + conffeqop, + numfkdelsetcols, + confdelsetcols, + false, /* no old check exists */ + AccessExclusiveLock, + insertTriggerOid, + updateTriggerOid, + with_period); + table_close(pkrel, NoLock); + } + + table_close(trigrel, RowExclusiveLock); +} + +/* + * GetForeignKeyActionTriggers + * Returns delete and update "action" triggers of the given relation + * belonging to the given constraint + */ +static void +GetForeignKeyActionTriggers(Relation trigrel, + Oid conoid, Oid confrelid, Oid conrelid, + Oid *deleteTriggerOid, + Oid *updateTriggerOid) +{ + ScanKeyData key; + SysScanDesc scan; + HeapTuple trigtup; + + *deleteTriggerOid = *updateTriggerOid = InvalidOid; + ScanKeyInit(&key, + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conoid)); + + scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true, + NULL, 1, &key); + while ((trigtup = systable_getnext(scan)) != NULL) + { + Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup); + + if (trgform->tgconstrrelid != conrelid) + continue; + if (trgform->tgrelid != confrelid) + continue; + /* Only ever look at "action" triggers on the PK side. */ + if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_PK) + continue; + if (TRIGGER_FOR_DELETE(trgform->tgtype)) + { + Assert(*deleteTriggerOid == InvalidOid); + *deleteTriggerOid = trgform->oid; + } + else if (TRIGGER_FOR_UPDATE(trgform->tgtype)) + { + Assert(*updateTriggerOid == InvalidOid); + *updateTriggerOid = trgform->oid; + } +#ifndef USE_ASSERT_CHECKING + /* In an assert-enabled build, continue looking to find duplicates */ + if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid)) + break; +#endif + } + + if (!OidIsValid(*deleteTriggerOid)) + elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u", + conoid); + if (!OidIsValid(*updateTriggerOid)) + elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u", + conoid); + + systable_endscan(scan); +} + +/* + * CloneFkReferenced + * Subroutine for CloneForeignKeyConstraints + * + * Find all the FKs that have the parent relation on the referenced side; + * clone those constraints to the given partition. This is to be called + * when the partition is being created or attached. + * + * This recurses to partitions, if the relation being attached is partitioned. + * Recursion is done by calling addFkRecurseReferenced. + */ +static void +CloneFkReferenced(Relation parentRel, Relation partitionRel) +{ + Relation pg_constraint; + AttrMap *attmap; + ListCell *cell; + SysScanDesc scan; + ScanKeyData key[2]; + HeapTuple tuple; + List *clone = NIL; + Relation trigrel; + + /* + * Search for any constraints where this partition's parent is in the + * referenced side. However, we must not clone any constraint whose + * parent constraint is also going to be cloned, to avoid duplicates. So + * do it in two steps: first construct the list of constraints to clone, + * then go over that list cloning those whose parents are not in the list. + * (We must not rely on the parent being seen first, since the catalog + * scan could return children first.) + */ + pg_constraint = table_open(ConstraintRelationId, RowShareLock); + ScanKeyInit(&key[0], + Anum_pg_constraint_confrelid, BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel))); + ScanKeyInit(&key[1], + Anum_pg_constraint_contype, BTEqualStrategyNumber, + F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); + /* This is a seqscan, as we don't have a usable index ... */ + scan = systable_beginscan(pg_constraint, InvalidOid, true, + NULL, 2, key); + while ((tuple = systable_getnext(scan)) != NULL) + { + Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + clone = lappend_oid(clone, constrForm->oid); + } + systable_endscan(scan); + table_close(pg_constraint, RowShareLock); + + /* + * Triggers of the foreign keys will be manipulated a bunch of times in + * the loop below. To avoid repeatedly opening/closing the trigger + * catalog relation, we open it here and pass it to the subroutines called + * below. + */ + trigrel = table_open(TriggerRelationId, RowExclusiveLock); + + attmap = build_attrmap_by_name(RelationGetDescr(partitionRel), + RelationGetDescr(parentRel), + false); + foreach(cell, clone) + { + Oid constrOid = lfirst_oid(cell); + Form_pg_constraint constrForm; + Relation fkRel; + Oid indexOid; + Oid partIndexId; + int numfks; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber mapped_confkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + Oid conpfeqop[INDEX_MAX_KEYS]; + Oid conppeqop[INDEX_MAX_KEYS]; + Oid conffeqop[INDEX_MAX_KEYS]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + Constraint *fkconstraint; + ObjectAddress address; + Oid deleteTriggerOid = InvalidOid, + updateTriggerOid = InvalidOid; + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", constrOid); + constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + /* + * As explained above: don't try to clone a constraint for which we're + * going to clone the parent. + */ + if (list_member_oid(clone, constrForm->conparentid)) + { + ReleaseSysCache(tuple); + continue; + } + + /* We need the same lock level that CreateTrigger will acquire */ + fkRel = table_open(constrForm->conrelid, ShareRowExclusiveLock); + + indexOid = constrForm->conindid; + DeconstructFkConstraintRow(tuple, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + &numfkdelsetcols, + confdelsetcols); + + for (int i = 0; i < numfks; i++) + mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; + + fkconstraint = makeNode(Constraint); + fkconstraint->contype = CONSTRAINT_FOREIGN; + fkconstraint->conname = NameStr(constrForm->conname); + fkconstraint->deferrable = constrForm->condeferrable; + fkconstraint->initdeferred = constrForm->condeferred; + fkconstraint->location = -1; + fkconstraint->pktable = NULL; + /* ->fk_attrs determined below */ + fkconstraint->pk_attrs = NIL; + fkconstraint->fk_matchtype = constrForm->confmatchtype; + fkconstraint->fk_upd_action = constrForm->confupdtype; + fkconstraint->fk_del_action = constrForm->confdeltype; + fkconstraint->fk_del_set_cols = NIL; + fkconstraint->old_conpfeqop = NIL; + fkconstraint->old_pktable_oid = InvalidOid; + fkconstraint->is_enforced = constrForm->conenforced; + fkconstraint->skip_validation = false; + fkconstraint->initially_valid = constrForm->convalidated; + + /* set up colnames that are used to generate the constraint name */ + for (int i = 0; i < numfks; i++) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(fkRel), + conkey[i] - 1); + fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, + makeString(NameStr(att->attname))); + } + + /* + * Add the new foreign key constraint pointing to the new partition. + * Because this new partition appears in the referenced side of the + * constraint, we don't need to set up for Phase 3 check. + */ + partIndexId = index_get_partition(partitionRel, indexOid); + if (!OidIsValid(partIndexId)) + elog(ERROR, "index for %u not found in partition %s", + indexOid, RelationGetRelationName(partitionRel)); + + /* + * Get the "action" triggers belonging to the constraint to pass as + * parent OIDs for similar triggers that will be created on the + * partition in addFkRecurseReferenced(). + */ + if (constrForm->conenforced) + GetForeignKeyActionTriggers(trigrel, constrOid, + constrForm->confrelid, constrForm->conrelid, + &deleteTriggerOid, &updateTriggerOid); + + /* Add this constraint ... */ + address = addFkConstraint(addFkReferencedSide, + fkconstraint->conname, fkconstraint, fkRel, + partitionRel, partIndexId, constrOid, + numfks, mapped_confkey, + conkey, conpfeqop, conppeqop, conffeqop, + numfkdelsetcols, confdelsetcols, false, + constrForm->conperiod); + /* ... and recurse */ + addFkRecurseReferenced(fkconstraint, + fkRel, + partitionRel, + partIndexId, + address.objectId, + numfks, + mapped_confkey, + conkey, + conpfeqop, + conppeqop, + conffeqop, + numfkdelsetcols, + confdelsetcols, + true, + deleteTriggerOid, + updateTriggerOid, + constrForm->conperiod); + + table_close(fkRel, NoLock); + ReleaseSysCache(tuple); + } + + table_close(trigrel, RowExclusiveLock); +} + +/* + * CloneForeignKeyConstraints + * Clone foreign keys from a partitioned table to a newly acquired + * partition. + * + * partitionRel is a partition of parentRel, so we can be certain that it has + * the same columns with the same datatypes. The columns may be in different + * order, though. + * + * wqueue must be passed to set up phase 3 constraint checking, unless the + * referencing-side partition is known to be empty (such as in CREATE TABLE / + * PARTITION OF). + */ +void +CloneForeignKeyConstraints(List **wqueue, Relation parentRel, + Relation partitionRel) +{ + /* This only works for declarative partitioning */ + Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + /* + * First, clone constraints where the parent is on the referencing side. + */ + CloneFkReferencing(wqueue, parentRel, partitionRel); + + /* + * Clone constraints for which the parent is on the referenced side. + */ + CloneFkReferenced(parentRel, partitionRel); +} + +/* + * MarkInheritDetached + * + * Set inhdetachpending for a partition, for ATExecDetachPartition + * in concurrent mode. While at it, verify that no other partition is + * already pending detach. + */ +static void +MarkInheritDetached(Relation child_rel, Relation parent_rel) +{ + Relation catalogRelation; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + bool found = false; + + Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + /* + * Find pg_inherits entries by inhparent. (We need to scan them all in + * order to verify that no other partition is pending detach.) + */ + catalogRelation = table_open(InheritsRelationId, RowExclusiveLock); + ScanKeyInit(&key, + Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(parent_rel))); + scan = systable_beginscan(catalogRelation, InheritsParentIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inhForm; + + inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + if (inhForm->inhdetachpending) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"", + get_rel_name(inhForm->inhrelid), + get_namespace_name(parent_rel->rd_rel->relnamespace), + RelationGetRelationName(parent_rel)), + errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation.")); + + if (inhForm->inhrelid == RelationGetRelid(child_rel)) + { + HeapTuple newtup; + + newtup = heap_copytuple(inheritsTuple); + ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true; + + CatalogTupleUpdate(catalogRelation, + &inheritsTuple->t_self, + newtup); + found = true; + heap_freetuple(newtup); + /* keep looking, to ensure we catch others pending detach */ + } + } + + /* Done */ + systable_endscan(scan); + table_close(catalogRelation, RowExclusiveLock); + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation \"%s\" is not a partition of relation \"%s\"", + RelationGetRelationName(child_rel), + RelationGetRelationName(parent_rel)))); +} + +/* + * Transform any expressions present in the partition key + * + * Returns a transformed PartitionSpec. + */ +PartitionSpec * +transformPartitionSpec(Relation rel, PartitionSpec *partspec) +{ + PartitionSpec *newspec; + ParseState *pstate; + ParseNamespaceItem *nsitem; + ListCell *l; + + newspec = makeNode(PartitionSpec); + + newspec->strategy = partspec->strategy; + newspec->partParams = NIL; + newspec->location = partspec->location; + + /* Check valid number of columns for strategy */ + if (partspec->strategy == PARTITION_STRATEGY_LIST && + list_length(partspec->partParams) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use \"list\" partition strategy with more than one column"))); + + /* + * Create a dummy ParseState and insert the target relation as its sole + * rangetable entry. We need a ParseState for transformExpr. + */ + pstate = make_parsestate(NULL); + nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock, + NULL, false, true); + addNSItemToQuery(pstate, nsitem, true, true, true); + + /* take care of any partition expressions */ + foreach(l, partspec->partParams) + { + PartitionElem *pelem = lfirst_node(PartitionElem, l); + + if (pelem->expr) + { + /* Copy, to avoid scribbling on the input */ + pelem = copyObject(pelem); + + /* Now do parse transformation of the expression */ + pelem->expr = transformExpr(pstate, pelem->expr, + EXPR_KIND_PARTITION_EXPRESSION); + + /* we have to fix its collations too */ + assign_expr_collations(pstate, pelem->expr); + } + + newspec->partParams = lappend(newspec->partParams, pelem); + } + + return newspec; +} + +/* + * Compute per-partition-column information from a list of PartitionElems. + * Expressions in the PartitionElems must be parse-analyzed already. + */ +void +ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, + List **partexprs, Oid *partopclass, Oid *partcollation, + PartitionStrategy strategy) +{ + int attn; + ListCell *lc; + Oid am_oid; + + attn = 0; + foreach(lc, partParams) + { + PartitionElem *pelem = lfirst_node(PartitionElem, lc); + Oid atttype; + Oid attcollation; + + if (pelem->name != NULL) + { + /* Simple attribute reference */ + HeapTuple atttuple; + Form_pg_attribute attform; + + atttuple = SearchSysCacheAttName(RelationGetRelid(rel), + pelem->name); + if (!HeapTupleIsValid(atttuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" named in partition key does not exist", + pelem->name), + parser_errposition(pstate, pelem->location))); + attform = (Form_pg_attribute) GETSTRUCT(atttuple); + + if (attform->attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use system column \"%s\" in partition key", + pelem->name), + parser_errposition(pstate, pelem->location))); + + /* + * Stored generated columns cannot work: They are computed after + * BEFORE triggers, but partition routing is done before all + * triggers. Maybe virtual generated columns could be made to + * work, but then they would need to be handled as an expression + * below. + */ + if (attform->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use generated column in partition key"), + errdetail("Column \"%s\" is a generated column.", + pelem->name), + parser_errposition(pstate, pelem->location))); + + partattrs[attn] = attform->attnum; + atttype = attform->atttypid; + attcollation = attform->attcollation; + ReleaseSysCache(atttuple); + } + else + { + /* Expression */ + Node *expr = pelem->expr; + char partattname[16]; + Bitmapset *expr_attrs = NULL; + int i; + + Assert(expr != NULL); + atttype = exprType(expr); + attcollation = exprCollation(expr); + + /* + * The expression must be of a storable type (e.g., not RECORD). + * The test is the same as for whether a table column is of a safe + * type (which is why we needn't check for the non-expression + * case). + */ + snprintf(partattname, sizeof(partattname), "%d", attn + 1); + CheckAttributeType(partattname, + atttype, attcollation, + NIL, CHKATYPE_IS_PARTKEY); + + /* + * Strip any top-level COLLATE clause. This ensures that we treat + * "x COLLATE y" and "(x COLLATE y)" alike. + */ + while (IsA(expr, CollateExpr)) + expr = (Node *) ((CollateExpr *) expr)->arg; + + /* + * Examine all the columns in the partition key expression. When + * the whole-row reference is present, examine all the columns of + * the partitioned table. + */ + pull_varattnos(expr, 1, &expr_attrs); + if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + expr_attrs = bms_add_range(expr_attrs, + 1 - FirstLowInvalidHeapAttributeNumber, + RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber); + expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber); + } + + i = -1; + while ((i = bms_next_member(expr_attrs, i)) >= 0) + { + AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; + + Assert(attno != 0); + + /* + * Cannot allow system column references, since that would + * make partition routing impossible: their values won't be + * known yet when we need to do that. + */ + if (attno < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("partition key expressions cannot contain system column references"))); + + /* + * Stored generated columns cannot work: They are computed + * after BEFORE triggers, but partition routing is done before + * all triggers. Virtual generated columns could probably + * work, but it would require more work elsewhere (for example + * SET EXPRESSION would need to check whether the column is + * used in partition keys). Seems safer to prohibit for now. + */ + if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use generated column in partition key"), + errdetail("Column \"%s\" is a generated column.", + get_attname(RelationGetRelid(rel), attno, false)), + parser_errposition(pstate, pelem->location))); + } + + if (IsA(expr, Var) && + ((Var *) expr)->varattno > 0) + { + + /* + * User wrote "(column)" or "(column COLLATE something)". + * Treat it like simple attribute anyway. + */ + partattrs[attn] = ((Var *) expr)->varattno; + } + else + { + partattrs[attn] = 0; /* marks the column as expression */ + *partexprs = lappend(*partexprs, expr); + + /* + * transformPartitionSpec() should have already rejected + * subqueries, aggregates, window functions, and SRFs, based + * on the EXPR_KIND_ for partition expressions. + */ + + /* + * Preprocess the expression before checking for mutability. + * This is essential for the reasons described in + * contain_mutable_functions_after_planning. However, we call + * expression_planner for ourselves rather than using that + * function, because if constant-folding reduces the + * expression to a constant, we'd like to know that so we can + * complain below. + * + * Like contain_mutable_functions_after_planning, assume that + * expression_planner won't scribble on its input, so this + * won't affect the partexprs entry we saved above. + */ + expr = (Node *) expression_planner((Expr *) expr); + + /* + * Partition expressions cannot contain mutable functions, + * because a given row must always map to the same partition + * as long as there is no change in the partition boundary + * structure. + */ + if (contain_mutable_functions(expr)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("functions in partition key expression must be marked IMMUTABLE"))); + + /* + * While it is not exactly *wrong* for a partition expression + * to be a constant, it seems better to reject such keys. + */ + if (IsA(expr, Const)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use constant expression as partition key"))); + } + } + + /* + * Apply collation override if any + */ + if (pelem->collation) + attcollation = get_collation_oid(pelem->collation, false); + + /* + * Check we have a collation iff it's a collatable type. The only + * expected failures here are (1) COLLATE applied to a noncollatable + * type, or (2) partition expression had an unresolved collation. But + * we might as well code this to be a complete consistency check. + */ + if (type_is_collatable(atttype)) + { + if (!OidIsValid(attcollation)) + ereport(ERROR, + (errcode(ERRCODE_INDETERMINATE_COLLATION), + errmsg("could not determine which collation to use for partition expression"), + errhint("Use the COLLATE clause to set the collation explicitly."))); + } + else + { + if (OidIsValid(attcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(atttype)))); + } + + partcollation[attn] = attcollation; + + /* + * Identify the appropriate operator class. For list and range + * partitioning, we use a btree operator class; hash partitioning uses + * a hash operator class. + */ + if (strategy == PARTITION_STRATEGY_HASH) + am_oid = HASH_AM_OID; + else + am_oid = BTREE_AM_OID; + + if (!pelem->opclass) + { + partopclass[attn] = GetDefaultOpClass(atttype, am_oid); + + if (!OidIsValid(partopclass[attn])) + { + if (strategy == PARTITION_STRATEGY_HASH) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("data type %s has no default operator class for access method \"%s\"", + format_type_be(atttype), "hash"), + errhint("You must specify a hash operator class or define a default hash operator class for the data type."))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("data type %s has no default operator class for access method \"%s\"", + format_type_be(atttype), "btree"), + errhint("You must specify a btree operator class or define a default btree operator class for the data type."))); + } + } + else + partopclass[attn] = ResolveOpClass(pelem->opclass, + atttype, + am_oid == HASH_AM_OID ? "hash" : "btree", + am_oid); + + attn++; + } +} + +/* + * PartConstraintImpliedByRelConstraint + * Do scanrel's existing constraints imply the partition constraint? + * + * "Existing constraints" include its check constraints and column-level + * not-null constraints. partConstraint describes the partition constraint, + * in implicit-AND form. + */ +bool +PartConstraintImpliedByRelConstraint(Relation scanrel, + List *partConstraint) +{ + List *existConstraint = NIL; + TupleConstr *constr = RelationGetDescr(scanrel)->constr; + int i; + + if (constr && constr->has_not_null) + { + int natts = scanrel->rd_att->natts; + + for (i = 1; i <= natts; i++) + { + CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1); + + /* invalid not-null constraint must be ignored here */ + if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped) + { + Form_pg_attribute wholeatt = TupleDescAttr(scanrel->rd_att, i - 1); + NullTest *ntest = makeNode(NullTest); + + ntest->arg = (Expr *) makeVar(1, + i, + wholeatt->atttypid, + wholeatt->atttypmod, + wholeatt->attcollation, + 0); + ntest->nulltesttype = IS_NOT_NULL; + + /* + * argisrow=false is correct even for a composite column, + * because attnotnull does not represent a SQL-spec IS NOT + * NULL test in such a case, just IS DISTINCT FROM NULL. + */ + ntest->argisrow = false; + ntest->location = -1; + existConstraint = lappend(existConstraint, ntest); + } + } + } + + return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint); +} + +/* + * ConstraintImpliedByRelConstraint + * Do scanrel's existing constraints imply the given constraint? + * + * testConstraint is the constraint to validate. provenConstraint is a + * caller-provided list of conditions which this function may assume + * to be true. Both provenConstraint and testConstraint must be in + * implicit-AND form, must only contain immutable clauses, and must + * contain only Vars with varno = 1. + */ +bool +ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint) +{ + List *existConstraint = list_copy(provenConstraint); + TupleConstr *constr = RelationGetDescr(scanrel)->constr; + int num_check, + i; + + num_check = (constr != NULL) ? constr->num_check : 0; + for (i = 0; i < num_check; i++) + { + Node *cexpr; + + /* + * If this constraint hasn't been fully validated yet, we must ignore + * it here. + */ + if (!constr->check[i].ccvalid) + continue; + + /* + * NOT ENFORCED constraints are always marked as invalid, which should + * have been ignored. + */ + Assert(constr->check[i].ccenforced); + + cexpr = stringToNode(constr->check[i].ccbin); + + /* + * Run each expression through const-simplification and + * canonicalization. It is necessary, because we will be comparing it + * to similarly-processed partition constraint expressions, and may + * fail to detect valid matches without this. + */ + cexpr = eval_const_expressions(NULL, cexpr); + cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true); + + existConstraint = list_concat(existConstraint, + make_ands_implicit((Expr *) cexpr)); + } + + /* + * Try to make the proof. Since we are comparing CHECK constraints, we + * need to use weak implication, i.e., we assume existConstraint is + * not-false and try to prove the same for testConstraint. + * + * Note that predicate_implied_by assumes its first argument is known + * immutable. That should always be true for both NOT NULL and partition + * constraints, so we don't test it here. + */ + return predicate_implied_by(testConstraint, existConstraint, true); +} + +/* + * QueuePartitionConstraintValidation + * + * Add an entry to wqueue to have the given partition constraint validated by + * Phase 3, for the given relation, and all its children. + * + * We first verify whether the given constraint is implied by pre-existing + * relation constraints; if it is, there's no need to scan the table to + * validate, so don't queue in that case. + */ +static void +QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, + List *partConstraint, + bool validate_default) +{ + /* + * Based on the table's existing constraints, determine whether or not we + * may skip scanning the table. + */ + if (PartConstraintImpliedByRelConstraint(scanrel, partConstraint)) + { + if (!validate_default) + ereport(DEBUG1, + (errmsg_internal("partition constraint for table \"%s\" is implied by existing constraints", + RelationGetRelationName(scanrel)))); + else + ereport(DEBUG1, + (errmsg_internal("updated partition constraint for default partition \"%s\" is implied by existing constraints", + RelationGetRelationName(scanrel)))); + return; + } + + /* + * Constraints proved insufficient. For plain relations, queue a + * validation item now; for partitioned tables, recurse to process each + * partition. + */ + if (scanrel->rd_rel->relkind == RELKIND_RELATION) + { + AlteredTableInfo *tab; + + /* Grab a work queue entry. */ + tab = ATGetQueueEntry(wqueue, scanrel); + Assert(tab->partition_constraint == NULL); + tab->partition_constraint = (Expr *) linitial(partConstraint); + tab->validate_default = validate_default; + } + else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, true); + int i; + + for (i = 0; i < partdesc->nparts; i++) + { + Relation part_rel; + List *thisPartConstraint; + + /* + * This is the minimum lock we need to prevent deadlocks. + */ + part_rel = table_open(partdesc->oids[i], AccessExclusiveLock); + + /* + * Adjust the constraint for scanrel so that it matches this + * partition's attribute numbers. + */ + thisPartConstraint = + map_partition_varattnos(partConstraint, 1, + part_rel, scanrel); + + QueuePartitionConstraintValidation(wqueue, part_rel, + thisPartConstraint, + validate_default); + table_close(part_rel, NoLock); /* keep lock till commit */ + } + } +} + +/* + * AttachPartitionEnsureIndexes + * subroutine for ATExecAttachPartition to create/match indexes + * + * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH + * PARTITION: every partition must have an index attached to each index on the + * partitioned table. + */ +static void +AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) +{ + List *idxes; + List *attachRelIdxs; + Relation *attachrelIdxRels; + IndexInfo **attachInfos; + ListCell *cell; + MemoryContext cxt; + MemoryContext oldcxt; + + cxt = AllocSetContextCreate(CurrentMemoryContext, + "AttachPartitionEnsureIndexes", + ALLOCSET_DEFAULT_SIZES); + oldcxt = MemoryContextSwitchTo(cxt); + + idxes = RelationGetIndexList(rel); + attachRelIdxs = RelationGetIndexList(attachrel); + attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs)); + attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs)); + + /* Build arrays of all existing indexes and their IndexInfos */ + foreach_oid(cldIdxId, attachRelIdxs) + { + int i = foreach_current_index(cldIdxId); + + attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock); + attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]); + } + + /* + * If we're attaching a foreign table, we must fail if any of the indexes + * is a constraint index; otherwise, there's nothing to do here. Do this + * before starting work, to avoid wasting the effort of building a few + * non-unique indexes before coming across a unique one. + */ + if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + { + foreach(cell, idxes) + { + Oid idx = lfirst_oid(cell); + Relation idxRel = index_open(idx, AccessShareLock); + + if (idxRel->rd_index->indisunique || + idxRel->rd_index->indisprimary) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach foreign table \"%s\" as partition of partitioned table \"%s\"", + RelationGetRelationName(attachrel), + RelationGetRelationName(rel)), + errdetail("Partitioned table \"%s\" contains unique indexes.", + RelationGetRelationName(rel)))); + index_close(idxRel, AccessShareLock); + } + + goto out; + } + + /* + * For each index on the partitioned table, find a matching one in the + * partition-to-be; if one is not found, create one. + */ + foreach(cell, idxes) + { + Oid idx = lfirst_oid(cell); + Relation idxRel = index_open(idx, AccessShareLock); + IndexInfo *info; + AttrMap *attmap; + bool found = false; + Oid constraintOid; + + /* + * Ignore indexes in the partitioned table other than partitioned + * indexes. + */ + if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) + { + index_close(idxRel, AccessShareLock); + continue; + } + + /* construct an indexinfo to compare existing indexes against */ + info = BuildIndexInfo(idxRel); + attmap = build_attrmap_by_name(RelationGetDescr(attachrel), + RelationGetDescr(rel), + false); + constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), idx); + + /* + * Scan the list of existing indexes in the partition-to-be, and mark + * the first matching, valid, unattached one we find, if any, as + * partition of the parent index. If we find one, we're done. + */ + for (int i = 0; i < list_length(attachRelIdxs); i++) + { + Oid cldIdxId = RelationGetRelid(attachrelIdxRels[i]); + Oid cldConstrOid = InvalidOid; + + /* does this index have a parent? if so, can't use it */ + if (attachrelIdxRels[i]->rd_rel->relispartition) + continue; + + /* If this index is invalid, can't use it */ + if (!attachrelIdxRels[i]->rd_index->indisvalid) + continue; + + if (CompareIndexInfo(attachInfos[i], info, + attachrelIdxRels[i]->rd_indcollation, + idxRel->rd_indcollation, + attachrelIdxRels[i]->rd_opfamily, + idxRel->rd_opfamily, + attmap)) + { + /* + * If this index is being created in the parent because of a + * constraint, then the child needs to have a constraint also, + * so look for one. If there is no such constraint, this + * index is no good, so keep looking. + */ + if (OidIsValid(constraintOid)) + { + cldConstrOid = + get_relation_idx_constraint_oid(RelationGetRelid(attachrel), + cldIdxId); + /* no dice */ + if (!OidIsValid(cldConstrOid)) + continue; + + /* Ensure they're both the same type of constraint */ + if (get_constraint_type(constraintOid) != + get_constraint_type(cldConstrOid)) + continue; + } + + /* bingo. */ + IndexSetParentIndex(attachrelIdxRels[i], idx); + if (OidIsValid(constraintOid)) + ConstraintSetParentConstraint(cldConstrOid, constraintOid, + RelationGetRelid(attachrel)); + found = true; + + CommandCounterIncrement(); + break; + } + } + + /* + * If no suitable index was found in the partition-to-be, create one + * now. Note that if this is a PK, not-null constraints must already + * exist. + */ + if (!found) + { + IndexStmt *stmt; + Oid conOid; + + stmt = generateClonedIndexStmt(NULL, + idxRel, attmap, + &conOid); + DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid, + RelationGetRelid(idxRel), + conOid, + -1, + true, false, false, false, false); + } + + index_close(idxRel, AccessShareLock); + } + +out: + /* Clean up. */ + for (int i = 0; i < list_length(attachRelIdxs); i++) + index_close(attachrelIdxRels[i], AccessShareLock); + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(cxt); +} + +/* + * ALTER TABLE ATTACH PARTITION FOR VALUES + * + * Return the address of the newly attached partition. + */ +ObjectAddress +ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, + AlterTableUtilityContext *context) +{ + Relation attachrel, + catalog; + List *attachrel_children; + List *partConstraint; + SysScanDesc scan; + ScanKeyData skey; + AttrNumber attno; + int natts; + TupleDesc tupleDesc; + ObjectAddress address; + const char *trigger_name; + Oid defaultPartOid; + List *partBoundConstraint; + ParseState *pstate = make_parsestate(NULL); + + pstate->p_sourcetext = context->queryString; + + /* + * We must lock the default partition if one exists, because attaching a + * new partition will change its partition constraint. + */ + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + if (OidIsValid(defaultPartOid)) + LockRelationOid(defaultPartOid, AccessExclusiveLock); + + attachrel = table_openrv(cmd->name, AccessExclusiveLock); + + /* + * XXX I think it'd be a good idea to grab locks on all tables referenced + * by FKs at this point also. + */ + + /* + * Must be owner of both parent and source table -- parent was checked by + * ATSimplePermissions call in ATPrepCmd + */ + ATSimplePermissions(AT_AttachPartition, attachrel, + ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + + /* A partition can only have one parent */ + if (attachrel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is already a partition", + RelationGetRelationName(attachrel)))); + + if (OidIsValid(attachrel->rd_rel->reloftype)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a typed table as partition"))); + + /* + * Table being attached should not already be part of inheritance; either + * as a child table... + */ + catalog = table_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachrel))); + scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true, + NULL, 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan))) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach inheritance child as partition"))); + systable_endscan(scan); + + /* ...or as a parent table (except the case when it is partitioned) */ + ScanKeyInit(&skey, + Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(attachrel))); + scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL, + 1, &skey); + if (HeapTupleIsValid(systable_getnext(scan)) && + attachrel->rd_rel->relkind == RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach inheritance parent as partition"))); + systable_endscan(scan); + table_close(catalog, AccessShareLock); + + /* + * Prevent circularity by seeing if rel is a partition of attachrel. (In + * particular, this disallows making a rel a partition of itself.) + * + * We do that by checking if rel is a member of the list of attachrel's + * partitions provided the latter is partitioned at all. We want to avoid + * having to construct this list again, so we request the strongest lock + * on all partitions. We need the strongest lock, because we may decide + * to scan them if we find out that the table being attached (or its leaf + * partitions) may contain rows that violate the partition constraint. If + * the table has a constraint that would prevent such rows, which by + * definition is present in all the partitions, we need not scan the + * table, nor its partitions. But we cannot risk a deadlock by taking a + * weaker lock now and the stronger one only when needed. + */ + attachrel_children = find_all_inheritors(RelationGetRelid(attachrel), + AccessExclusiveLock, NULL); + if (list_member_oid(attachrel_children, RelationGetRelid(rel))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("circular inheritance not allowed"), + errdetail("\"%s\" is already a child of \"%s\".", + RelationGetRelationName(rel), + RelationGetRelationName(attachrel)))); + + /* If the parent is permanent, so must be all of its partitions. */ + if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP && + attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"", + RelationGetRelationName(rel)))); + + /* Temp parent cannot have a partition that is itself not a temp */ + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && + attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"", + RelationGetRelationName(rel)))); + + /* If the parent is temp, it must belong to this session */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach as partition of temporary relation of another session"))); + + /* Ditto for the partition */ + if (RELATION_IS_OTHER_TEMP(attachrel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot attach temporary relation of another session as partition"))); + + /* + * Check if attachrel has any identity columns or any columns that aren't + * in the parent. + */ + tupleDesc = RelationGetDescr(attachrel); + natts = tupleDesc->natts; + for (attno = 1; attno <= natts; attno++) + { + Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1); + char *attributeName = NameStr(attribute->attname); + + /* Ignore dropped */ + if (attribute->attisdropped) + continue; + + if (attribute->attidentity) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("table \"%s\" being attached contains an identity column \"%s\"", + RelationGetRelationName(attachrel), attributeName), + errdetail("The new partition may not contain an identity column.")); + + /* Try to find the column in parent (matching on column name) */ + if (!SearchSysCacheExists2(ATTNAME, + ObjectIdGetDatum(RelationGetRelid(rel)), + CStringGetDatum(attributeName))) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"", + RelationGetRelationName(attachrel), attributeName, + RelationGetRelationName(rel)), + errdetail("The new partition may contain only the columns present in parent."))); + } + + /* + * If child_rel has row-level triggers with transition tables, we + * currently don't allow it to become a partition. See also prohibitions + * in ATExecAddInherit() and CreateTrigger(). + */ + trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc); + if (trigger_name != NULL) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition", + trigger_name, RelationGetRelationName(attachrel)), + errdetail("ROW triggers with transition tables are not supported on partitions."))); + + /* + * Check that the new partition's bound is valid and does not overlap any + * of existing partitions of the parent - note that it does not return on + * error. + */ + check_new_partition_bound(RelationGetRelationName(attachrel), rel, + cmd->bound, pstate); + + /* OK to create inheritance. Rest of the checks performed there */ + CreateInheritance(attachrel, rel, true); + + /* Update the pg_class entry. */ + StorePartitionBound(attachrel, rel, cmd->bound); + + /* Ensure there exists a correct set of indexes in the partition. */ + AttachPartitionEnsureIndexes(wqueue, rel, attachrel); + + /* and triggers */ + CloneRowTriggersToPartition(rel, attachrel); + + /* + * Clone foreign key constraints. Callee is responsible for setting up + * for phase 3 constraint verification. + */ + CloneForeignKeyConstraints(wqueue, rel, attachrel); + + /* + * Generate partition constraint from the partition bound specification. + * If the parent itself is a partition, make sure to include its + * constraint as well. + */ + partBoundConstraint = get_qual_from_partbound(rel, cmd->bound); + + /* + * Use list_concat_copy() to avoid modifying partBoundConstraint in place, + * since it's needed later to construct the constraint expression for + * validating against the default partition, if any. + */ + partConstraint = list_concat_copy(partBoundConstraint, + RelationGetPartitionQual(rel)); + + /* Skip validation if there are no constraints to validate. */ + if (partConstraint) + { + /* + * Run the partition quals through const-simplification similar to + * check constraints. We skip canonicalize_qual, though, because + * partition quals should be in canonical form already. + */ + partConstraint = + (List *) eval_const_expressions(NULL, + (Node *) partConstraint); + + /* XXX this sure looks wrong */ + partConstraint = list_make1(make_ands_explicit(partConstraint)); + + /* + * Adjust the generated constraint to match this partition's attribute + * numbers. + */ + partConstraint = map_partition_varattnos(partConstraint, 1, attachrel, + rel); + + /* Validate partition constraints against the table being attached. */ + QueuePartitionConstraintValidation(wqueue, attachrel, partConstraint, + false); + } + + /* + * If we're attaching a partition other than the default partition and a + * default one exists, then that partition's partition constraint changes, + * so add an entry to the work queue to validate it, too. (We must not do + * this when the partition being attached is the default one; we already + * did it above!) + */ + if (OidIsValid(defaultPartOid)) + { + Relation defaultrel; + List *defPartConstraint; + + Assert(!cmd->bound->is_default); + + /* we already hold a lock on the default partition */ + defaultrel = table_open(defaultPartOid, NoLock); + defPartConstraint = + get_proposed_default_constraint(partBoundConstraint); + + /* + * Map the Vars in the constraint expression from rel's attnos to + * defaultrel's. + */ + defPartConstraint = + map_partition_varattnos(defPartConstraint, + 1, defaultrel, rel); + QueuePartitionConstraintValidation(wqueue, defaultrel, + defPartConstraint, true); + + /* keep our lock until commit. */ + table_close(defaultrel, NoLock); + } + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel)); + + /* + * If the partition we just attached is partitioned itself, invalidate + * relcache for all descendent partitions too to ensure that their + * rd_partcheck expression trees are rebuilt; partitions already locked at + * the beginning of this function. + */ + if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + ListCell *l; + + foreach(l, attachrel_children) + { + CacheInvalidateRelcacheByRelid(lfirst_oid(l)); + } + } + + /* keep our lock until commit */ + table_close(attachrel, NoLock); + + return address; +} + +/* + * CloneRowTriggersToPartition + * subroutine for ATExecAttachPartition/DefineRelation to create row + * triggers on partitions + */ +void +CloneRowTriggersToPartition(Relation parent, Relation partition) +{ + Relation pg_trigger; + ScanKeyData key; + SysScanDesc scan; + HeapTuple tuple; + MemoryContext perTupCxt; + + ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent))); + pg_trigger = table_open(TriggerRelationId, RowExclusiveLock); + scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId, + true, NULL, 1, &key); + + perTupCxt = AllocSetContextCreate(CurrentMemoryContext, + "clone trig", ALLOCSET_SMALL_SIZES); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_trigger trigForm = (Form_pg_trigger) GETSTRUCT(tuple); + CreateTrigStmt *trigStmt; + Node *qual = NULL; + Datum value; + bool isnull; + List *cols = NIL; + List *trigargs = NIL; + MemoryContext oldcxt; + + /* + * Ignore statement-level triggers; those are not cloned. + */ + if (!TRIGGER_FOR_ROW(trigForm->tgtype)) + continue; + + /* + * Don't clone internal triggers, because the constraint cloning code + * will. + */ + if (trigForm->tgisinternal) + continue; + + /* + * Complain if we find an unexpected trigger type. + */ + if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) && + !TRIGGER_FOR_AFTER(trigForm->tgtype)) + elog(ERROR, "unexpected trigger \"%s\" found", + NameStr(trigForm->tgname)); + + /* Use short-lived context for CREATE TRIGGER */ + oldcxt = MemoryContextSwitchTo(perTupCxt); + + /* + * If there is a WHEN clause, generate a 'cooked' version of it that's + * appropriate for the partition. + */ + value = heap_getattr(tuple, Anum_pg_trigger_tgqual, + RelationGetDescr(pg_trigger), &isnull); + if (!isnull) + { + qual = stringToNode(TextDatumGetCString(value)); + qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO, + partition, parent); + qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO, + partition, parent); + } + + /* + * If there is a column list, transform it to a list of column names. + * Note we don't need to map this list in any way ... + */ + if (trigForm->tgattr.dim1 > 0) + { + int i; + + for (i = 0; i < trigForm->tgattr.dim1; i++) + { + Form_pg_attribute col; + + col = TupleDescAttr(parent->rd_att, + trigForm->tgattr.values[i] - 1); + cols = lappend(cols, + makeString(pstrdup(NameStr(col->attname)))); + } + } + + /* Reconstruct trigger arguments list. */ + if (trigForm->tgnargs > 0) + { + char *p; + + value = heap_getattr(tuple, Anum_pg_trigger_tgargs, + RelationGetDescr(pg_trigger), &isnull); + if (isnull) + elog(ERROR, "tgargs is null for trigger \"%s\" in partition \"%s\"", + NameStr(trigForm->tgname), RelationGetRelationName(partition)); + + p = (char *) VARDATA_ANY(DatumGetByteaPP(value)); + + for (int i = 0; i < trigForm->tgnargs; i++) + { + trigargs = lappend(trigargs, makeString(pstrdup(p))); + p += strlen(p) + 1; + } + } + + trigStmt = makeNode(CreateTrigStmt); + trigStmt->replace = false; + trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint); + trigStmt->trigname = NameStr(trigForm->tgname); + trigStmt->relation = NULL; + trigStmt->funcname = NULL; /* passed separately */ + trigStmt->args = trigargs; + trigStmt->row = true; + trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK; + trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK; + trigStmt->columns = cols; + trigStmt->whenClause = NULL; /* passed separately */ + trigStmt->transitionRels = NIL; /* not supported at present */ + trigStmt->deferrable = trigForm->tgdeferrable; + trigStmt->initdeferred = trigForm->tginitdeferred; + trigStmt->constrrel = NULL; /* passed separately */ + + CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition), + trigForm->tgconstrrelid, InvalidOid, InvalidOid, + trigForm->tgfoid, trigForm->oid, qual, + false, true, trigForm->tgenabled); + + MemoryContextSwitchTo(oldcxt); + MemoryContextReset(perTupCxt); + } + + MemoryContextDelete(perTupCxt); + + systable_endscan(scan); + table_close(pg_trigger, RowExclusiveLock); +} + +/* + * Return an OID list of constraints that reference the given relation + * that are marked as having a parent constraints. + */ +static List * +GetParentedForeignKeyRefs(Relation partition) +{ + Relation pg_constraint; + HeapTuple tuple; + SysScanDesc scan; + ScanKeyData key[2]; + List *constraints = NIL; + + /* + * If no indexes, or no columns are referenceable by FKs, we can avoid the + * scan. + */ + if (RelationGetIndexList(partition) == NIL || + bms_is_empty(RelationGetIndexAttrBitmap(partition, + INDEX_ATTR_BITMAP_KEY))) + return NIL; + + /* Search for constraints referencing this table */ + pg_constraint = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_constraint_confrelid, BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition))); + ScanKeyInit(&key[1], + Anum_pg_constraint_contype, BTEqualStrategyNumber, + F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); + + /* XXX This is a seqscan, as we don't have a usable index */ + scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key); + while ((tuple = systable_getnext(scan)) != NULL) + { + Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + /* + * We only need to process constraints that are part of larger ones. + */ + if (!OidIsValid(constrForm->conparentid)) + continue; + + constraints = lappend_oid(constraints, constrForm->oid); + } + + systable_endscan(scan); + table_close(pg_constraint, AccessShareLock); + + return constraints; +} + +/* + * During DETACH PARTITION, verify that any foreign keys pointing to the + * partitioned table would not become invalid. An error is raised if any + * referenced values exist. + */ +static void +ATDetachCheckNoForeignKeyRefs(Relation partition) +{ + List *constraints; + ListCell *cell; + + constraints = GetParentedForeignKeyRefs(partition); + + foreach(cell, constraints) + { + Oid constrOid = lfirst_oid(cell); + HeapTuple tuple; + Form_pg_constraint constrForm; + Relation rel; + Trigger trig = {0}; + + tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for constraint %u", constrOid); + constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + Assert(OidIsValid(constrForm->conparentid)); + Assert(constrForm->confrelid == RelationGetRelid(partition)); + + /* prevent data changes into the referencing table until commit */ + rel = table_open(constrForm->conrelid, ShareLock); + + trig.tgoid = InvalidOid; + trig.tgname = NameStr(constrForm->conname); + trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; + trig.tgisinternal = true; + trig.tgconstrrelid = RelationGetRelid(partition); + trig.tgconstrindid = constrForm->conindid; + trig.tgconstraint = constrForm->oid; + trig.tgdeferrable = false; + trig.tginitdeferred = false; + /* we needn't fill in remaining fields */ + + RI_PartitionRemove_Check(&trig, rel, partition); + + ReleaseSysCache(tuple); + + table_close(rel, NoLock); + } +} + +/* + * DropClonedTriggersFromPartition + * subroutine for ATExecDetachPartition to remove any triggers that were + * cloned to the partition when it was created-as-partition or attached. + * This undoes what CloneRowTriggersToPartition did. + */ +static void +DropClonedTriggersFromPartition(Oid partitionId) +{ + ScanKeyData skey; + SysScanDesc scan; + HeapTuple trigtup; + Relation tgrel; + ObjectAddresses *objects; + + objects = new_object_addresses(); + + /* + * Scan pg_trigger to search for all triggers on this rel. + */ + ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(partitionId)); + tgrel = table_open(TriggerRelationId, RowExclusiveLock); + scan = systable_beginscan(tgrel, TriggerRelidNameIndexId, + true, NULL, 1, &skey); + while (HeapTupleIsValid(trigtup = systable_getnext(scan))) + { + Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup); + ObjectAddress trig; + + /* Ignore triggers that weren't cloned */ + if (!OidIsValid(pg_trigger->tgparentid)) + continue; + + /* + * Ignore internal triggers that are implementation objects of foreign + * keys, because these will be detached when the foreign keys + * themselves are. + */ + if (OidIsValid(pg_trigger->tgconstrrelid)) + continue; + + /* + * This is ugly, but necessary: remove the dependency markings on the + * trigger so that it can be removed. + */ + deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, + TriggerRelationId, + DEPENDENCY_PARTITION_PRI); + deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, + RelationRelationId, + DEPENDENCY_PARTITION_SEC); + + /* remember this trigger to remove it below */ + ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid); + add_exact_object_address(&trig, objects); + } + + /* make the dependency removal visible to the deletion below */ + CommandCounterIncrement(); + performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + /* done */ + free_object_addresses(objects); + systable_endscan(scan); + table_close(tgrel, RowExclusiveLock); +} + +/* + * Second part of ALTER TABLE .. DETACH. + * + * This is separate so that it can be run independently when the second + * transaction of the concurrent algorithm fails (crash or abort). + */ +static void +DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, + Oid defaultPartOid) +{ + Relation classRel; + List *fks; + ListCell *cell; + List *indexes; + Datum new_val[Natts_pg_class]; + bool new_null[Natts_pg_class], + new_repl[Natts_pg_class]; + HeapTuple tuple, + newtuple; + Relation trigrel = NULL; + List *fkoids = NIL; + + if (concurrent) + { + /* + * We can remove the pg_inherits row now. (In the non-concurrent case, + * this was already done). + */ + RemoveInheritance(partRel, rel, true); + } + + /* Drop any triggers that were cloned on creation/attach. */ + DropClonedTriggersFromPartition(RelationGetRelid(partRel)); + + /* + * Detach any foreign keys that are inherited. This includes creating + * additional action triggers. + */ + fks = copyObject(RelationGetFKeyList(partRel)); + if (fks != NIL) + trigrel = table_open(TriggerRelationId, RowExclusiveLock); + + /* + * It's possible that the partition being detached has a foreign key that + * references a partitioned table. When that happens, there are multiple + * pg_constraint rows for the partition: one points to the partitioned + * table itself, while the others point to each of its partitions. Only + * the topmost one is to be considered here; the child constraints must be + * left alone, because conceptually those aren't coming from our parent + * partitioned table, but from this partition itself. + * + * We implement this by collecting all the constraint OIDs in a first scan + * of the FK array, and skipping in the loop below those constraints whose + * parents are listed here. + */ + foreach_node(ForeignKeyCacheInfo, fk, fks) + fkoids = lappend_oid(fkoids, fk->conoid); + + foreach(cell, fks) + { + ForeignKeyCacheInfo *fk = lfirst(cell); + HeapTuple contup; + Form_pg_constraint conform; + + contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid)); + if (!HeapTupleIsValid(contup)) + elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); + conform = (Form_pg_constraint) GETSTRUCT(contup); + + /* + * Consider only inherited foreign keys, and only if their parents + * aren't in the list. + */ + if (conform->contype != CONSTRAINT_FOREIGN || + !OidIsValid(conform->conparentid) || + list_member_oid(fkoids, conform->conparentid)) + { + ReleaseSysCache(contup); + continue; + } + + /* + * The constraint on this table must be marked no longer a child of + * the parent's constraint, as do its check triggers. + */ + ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid); + + /* + * Also, look up the partition's "check" triggers corresponding to the + * ENFORCED constraint being detached and detach them from the parent + * triggers. NOT ENFORCED constraints do not have these triggers; + * therefore, this step is not needed. + */ + if (fk->conenforced) + { + Oid insertTriggerOid, + updateTriggerOid; + + GetForeignKeyCheckTriggers(trigrel, + fk->conoid, fk->confrelid, fk->conrelid, + &insertTriggerOid, &updateTriggerOid); + Assert(OidIsValid(insertTriggerOid)); + TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, + RelationGetRelid(partRel)); + Assert(OidIsValid(updateTriggerOid)); + TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, + RelationGetRelid(partRel)); + } + + /* + * Lastly, create the action triggers on the referenced table, using + * addFkRecurseReferenced, which requires some elaborate setup (so put + * it in a separate block). While at it, if the table is partitioned, + * that function will recurse to create the pg_constraint rows and + * action triggers for each partition. + * + * Note there's no need to do addFkConstraint() here, because the + * pg_constraint row already exists. + */ + { + Constraint *fkconstraint; + int numfks; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + Oid conpfeqop[INDEX_MAX_KEYS]; + Oid conppeqop[INDEX_MAX_KEYS]; + Oid conffeqop[INDEX_MAX_KEYS]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + Relation refdRel; + + DeconstructFkConstraintRow(contup, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + &numfkdelsetcols, + confdelsetcols); + + /* Create a synthetic node we'll use throughout */ + fkconstraint = makeNode(Constraint); + fkconstraint->contype = CONSTRAINT_FOREIGN; + fkconstraint->conname = pstrdup(NameStr(conform->conname)); + fkconstraint->deferrable = conform->condeferrable; + fkconstraint->initdeferred = conform->condeferred; + fkconstraint->is_enforced = conform->conenforced; + fkconstraint->skip_validation = true; + fkconstraint->initially_valid = conform->convalidated; + /* a few irrelevant fields omitted here */ + fkconstraint->pktable = NULL; + fkconstraint->fk_attrs = NIL; + fkconstraint->pk_attrs = NIL; + fkconstraint->fk_matchtype = conform->confmatchtype; + fkconstraint->fk_upd_action = conform->confupdtype; + fkconstraint->fk_del_action = conform->confdeltype; + fkconstraint->fk_del_set_cols = NIL; + fkconstraint->old_conpfeqop = NIL; + fkconstraint->old_pktable_oid = InvalidOid; + fkconstraint->location = -1; + + /* set up colnames, used to generate the constraint name */ + for (int i = 0; i < numfks; i++) + { + Form_pg_attribute att; + + att = TupleDescAttr(RelationGetDescr(partRel), + conkey[i] - 1); + + fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, + makeString(NameStr(att->attname))); + } + + refdRel = table_open(fk->confrelid, ShareRowExclusiveLock); + + addFkRecurseReferenced(fkconstraint, partRel, + refdRel, + conform->conindid, + fk->conoid, + numfks, + confkey, + conkey, + conpfeqop, + conppeqop, + conffeqop, + numfkdelsetcols, + confdelsetcols, + true, + InvalidOid, InvalidOid, + conform->conperiod); + table_close(refdRel, NoLock); /* keep lock till end of xact */ + } + + ReleaseSysCache(contup); + } + list_free_deep(fks); + if (trigrel) + table_close(trigrel, RowExclusiveLock); + + /* + * Any sub-constraints that are in the referenced-side of a larger + * constraint have to be removed. This partition is no longer part of the + * key space of the constraint. + */ + foreach(cell, GetParentedForeignKeyRefs(partRel)) + { + Oid constrOid = lfirst_oid(cell); + ObjectAddress constraint; + + ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); + deleteDependencyRecordsForClass(ConstraintRelationId, + constrOid, + ConstraintRelationId, + DEPENDENCY_INTERNAL); + CommandCounterIncrement(); + + ObjectAddressSet(constraint, ConstraintRelationId, constrOid); + performDeletion(&constraint, DROP_RESTRICT, 0); + } + + /* Now we can detach indexes */ + indexes = RelationGetIndexList(partRel); + foreach(cell, indexes) + { + Oid idxid = lfirst_oid(cell); + Oid parentidx; + Relation idx; + Oid constrOid; + Oid parentConstrOid; + + if (!has_superclass(idxid)) + continue; + + parentidx = get_partition_parent(idxid, false); + Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel))); + + idx = index_open(idxid, AccessExclusiveLock); + IndexSetParentIndex(idx, InvalidOid); + + /* + * If there's a constraint associated with the index, detach it too. + * Careful: it is possible for a constraint index in a partition to be + * the child of a non-constraint index, so verify whether the parent + * index does actually have a constraint. + */ + constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel), + idxid); + parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), + parentidx); + if (OidIsValid(parentConstrOid) && OidIsValid(constrOid)) + ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); + + index_close(idx, NoLock); + } + + /* Update pg_class tuple */ + classRel = table_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(partRel))); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", + RelationGetRelid(partRel)); + Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition); + + /* Clear relpartbound and reset relispartition */ + memset(new_val, 0, sizeof(new_val)); + memset(new_null, false, sizeof(new_null)); + memset(new_repl, false, sizeof(new_repl)); + new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0; + new_null[Anum_pg_class_relpartbound - 1] = true; + new_repl[Anum_pg_class_relpartbound - 1] = true; + newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel), + new_val, new_null, new_repl); + + ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false; + CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple); + heap_freetuple(newtuple); + table_close(classRel, RowExclusiveLock); + + /* + * Drop identity property from all identity columns of partition. + */ + for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++) + { + Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno); + + if (!attr->attisdropped && attr->attidentity) + ATExecDropIdentity(partRel, NameStr(attr->attname), false, + AccessExclusiveLock, true, true); + } + + if (OidIsValid(defaultPartOid)) + { + /* + * If the relation being detached is the default partition itself, + * remove it from the parent's pg_partitioned_table entry. + * + * If not, we must invalidate default partition's relcache entry, as + * in StorePartitionBound: its partition constraint depends on every + * other partition's partition constraint. + */ + if (RelationGetRelid(partRel) == defaultPartOid) + update_default_partition_oid(RelationGetRelid(rel), InvalidOid); + else + CacheInvalidateRelcacheByRelid(defaultPartOid); + } + + /* + * Invalidate the parent's relcache so that the partition is no longer + * included in its partition descriptor. + */ + CacheInvalidateRelcache(rel); + + /* + * If the partition we just detached is partitioned itself, invalidate + * relcache for all descendent partitions too to ensure that their + * rd_partcheck expression trees are rebuilt; must lock partitions before + * doing so, using the same lockmode as what partRel has been locked with + * by the caller. + */ + if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + List *children; + + children = find_all_inheritors(RelationGetRelid(partRel), + AccessExclusiveLock, NULL); + foreach(cell, children) + { + CacheInvalidateRelcacheByRelid(lfirst_oid(cell)); + } + } +} + +/* + * ALTER TABLE DETACH PARTITION + * + * Return the address of the relation that is no longer a partition of rel. + * + * If concurrent mode is requested, we run in two transactions. A side- + * effect is that this command cannot run in a multi-part ALTER TABLE. + * Currently, that's enforced by the grammar. + * + * The strategy for concurrency is to first modify the partition's + * pg_inherit catalog row to make it visible to everyone that the + * partition is detached, lock the partition against writes, and commit + * the transaction; anyone who requests the partition descriptor from + * that point onwards has to ignore such a partition. In a second + * transaction, we wait until all transactions that could have seen the + * partition as attached are gone, then we remove the rest of partition + * metadata (pg_inherits and pg_class.relpartbounds). + */ +ObjectAddress +ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, + RangeVar *name, bool concurrent) +{ + Relation partRel; + ObjectAddress address; + Oid defaultPartOid; + + /* + * We must lock the default partition, because detaching this partition + * will change its partition constraint. + */ + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + if (OidIsValid(defaultPartOid)) + { + /* + * Concurrent detaching when a default partition exists is not + * supported. The main problem is that the default partition + * constraint would change. And there's a definitional problem: what + * should happen to the tuples that are being inserted that belong to + * the partition being detached? Putting them on the partition being + * detached would be wrong, since they'd become "lost" after the + * detaching completes but we cannot put them in the default partition + * either until we alter its partition constraint. + * + * I think we could solve this problem if we effected the constraint + * change before committing the first transaction. But the lock would + * have to remain AEL and it would cause concurrent query planning to + * be blocked, so changing it that way would be even worse. + */ + if (concurrent) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot detach partitions concurrently when a default partition exists"))); + LockRelationOid(defaultPartOid, AccessExclusiveLock); + } + + /* + * In concurrent mode, the partition is locked with share-update-exclusive + * in the first transaction. This allows concurrent transactions to be + * doing DML to the partition. + */ + partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock : + AccessExclusiveLock); + + /* + * Check inheritance conditions and either delete the pg_inherits row (in + * non-concurrent mode) or just set the inhdetachpending flag. + */ + if (!concurrent) + RemoveInheritance(partRel, rel, false); + else + MarkInheritDetached(partRel, rel); + + /* + * Ensure that foreign keys still hold after this detach. This keeps + * locks on the referencing tables, which prevents concurrent transactions + * from adding rows that we wouldn't see. For this to work in concurrent + * mode, it is critical that the partition appears as no longer attached + * for the RI queries as soon as the first transaction commits. + */ + ATDetachCheckNoForeignKeyRefs(partRel); + + /* + * Concurrent mode has to work harder; first we add a new constraint to + * the partition that matches the partition constraint. Then we close our + * existing transaction, and in a new one wait for all processes to catch + * up on the catalog updates we've done so far; at that point we can + * complete the operation. + */ + if (concurrent) + { + Oid partrelid, + parentrelid; + LOCKTAG tag; + char *parentrelname; + char *partrelname; + + /* + * We're almost done now; the only traces that remain are the + * pg_inherits tuple and the partition's relpartbounds. Before we can + * remove those, we need to wait until all transactions that know that + * this is a partition are gone. + */ + + /* + * Remember relation OIDs to re-acquire them later; and relation names + * too, for error messages if something is dropped in between. + */ + partrelid = RelationGetRelid(partRel); + parentrelid = RelationGetRelid(rel); + parentrelname = MemoryContextStrdup(PortalContext, + RelationGetRelationName(rel)); + partrelname = MemoryContextStrdup(PortalContext, + RelationGetRelationName(partRel)); + + /* Invalidate relcache entries for the parent -- must be before close */ + CacheInvalidateRelcache(rel); + + table_close(partRel, NoLock); + table_close(rel, NoLock); + tab->rel = NULL; + + /* Make updated catalog entry visible */ + PopActiveSnapshot(); + CommitTransactionCommand(); + + StartTransactionCommand(); + + /* + * Now wait. This ensures that all queries that were planned + * including the partition are finished before we remove the rest of + * catalog entries. We don't need or indeed want to acquire this + * lock, though -- that would block later queries. + * + * We don't need to concern ourselves with waiting for a lock on the + * partition itself, since we will acquire AccessExclusiveLock below. + */ + SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid); + WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false); + + /* + * Now acquire locks in both relations again. Note they may have been + * removed in the meantime, so care is required. + */ + rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock); + partRel = try_relation_open(partrelid, AccessExclusiveLock); + + /* If the relations aren't there, something bad happened; bail out */ + if (rel == NULL) + { + if (partRel != NULL) /* shouldn't happen */ + elog(WARNING, "dangling partition \"%s\" remains, can't fix", + partrelname); + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("partitioned table \"%s\" was removed concurrently", + parentrelname))); + } + if (partRel == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("partition \"%s\" was removed concurrently", partrelname))); + + tab->rel = rel; + } + + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Do the final part of detaching */ + DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid); + + PopActiveSnapshot(); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); + + /* keep our lock until commit */ + table_close(partRel, NoLock); + + return address; +} + +/* + * ALTER TABLE ... DETACH PARTITION ... FINALIZE + * + * To use when a DETACH PARTITION command previously did not run to + * completion; this completes the detaching process. + */ +ObjectAddress +ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) +{ + Relation partRel; + ObjectAddress address; + Snapshot snap = GetActiveSnapshot(); + + partRel = table_openrv(name, AccessExclusiveLock); + + /* + * Wait until existing snapshots are gone. This is important if the + * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the + * user could immediately run DETACH FINALIZE without actually waiting for + * existing transactions. We must not complete the detach action until + * all such queries are complete (otherwise we would present them with an + * inconsistent view of catalogs). + */ + WaitForOlderSnapshots(snap->xmin, false); + + DetachPartitionFinalize(rel, partRel, true, InvalidOid); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); + + table_close(partRel, NoLock); + + return address; +} + +/* + * Before acquiring lock on an index, acquire the same lock on the owning + * table. + */ +struct AttachIndexCallbackState +{ + Oid partitionOid; + Oid parentTblOid; + bool lockedParentTbl; +}; + +static void +RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid, + void *arg) +{ + struct AttachIndexCallbackState *state; + Form_pg_class classform; + HeapTuple tuple; + + state = (struct AttachIndexCallbackState *) arg; + + if (!state->lockedParentTbl) + { + LockRelationOid(state->parentTblOid, AccessShareLock); + state->lockedParentTbl = true; + } + + /* + * If we previously locked some other heap, and the name we're looking up + * no longer refers to an index on that relation, release the now-useless + * lock. XXX maybe we should do *after* we verify whether the index does + * not actually belong to the same relation ... + */ + if (relOid != oldRelOid && OidIsValid(state->partitionOid)) + { + UnlockRelationOid(state->partitionOid, AccessShareLock); + state->partitionOid = InvalidOid; + } + + /* Didn't find a relation, so no need for locking or permission checks. */ + if (!OidIsValid(relOid)) + return; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + if (!HeapTupleIsValid(tuple)) + return; /* concurrently dropped, so nothing to do */ + classform = (Form_pg_class) GETSTRUCT(tuple); + if (classform->relkind != RELKIND_PARTITIONED_INDEX && + classform->relkind != RELKIND_INDEX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("\"%s\" is not an index", rv->relname))); + ReleaseSysCache(tuple); + + /* + * Since we need only examine the heap's tupledesc, an access share lock + * on it (preventing any DDL) is sufficient. + */ + state->partitionOid = IndexGetRelation(relOid, false); + LockRelationOid(state->partitionOid, AccessShareLock); +} + +/* + * Verify whether the given partition already contains an index attached + * to the given partitioned index. If so, raise an error. + */ +static void +refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl) +{ + Oid existingIdx; + + existingIdx = index_get_partition(partitionTbl, + RelationGetRelid(parentIdx)); + if (OidIsValid(existingIdx)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentIdx)), + errdetail("Another index \"%s\" is already attached for partition \"%s\".", + get_rel_name(existingIdx), + RelationGetRelationName(partitionTbl)))); +} + +/* + * When attaching an index as a partition of a partitioned index which is a + * primary key, verify that all the columns in the partition are marked NOT + * NULL. + */ +static void +verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) +{ + for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) + { + Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), + iinfo->ii_IndexAttrNumbers[i] - 1); + + if (!att->attnotnull) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("invalid primary key definition"), + errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", + NameStr(att->attname), + RelationGetRelationName(partition))); + } +} + +/* + * Verify whether the set of attached partition indexes to a parent index on + * a partitioned table is complete. If it is, mark the parent index valid. + * + * This should be called each time a partition index is attached. + */ +static void +validatePartitionedIndex(Relation partedIdx, Relation partedTbl) +{ + Relation inheritsRel; + SysScanDesc scan; + ScanKeyData key; + int tuples = 0; + HeapTuple inhTup; + bool updated = false; + + Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX); + + /* + * Scan pg_inherits for this parent index. Count each valid index we find + * (verifying the pg_index entry for each), and if we reach the total + * amount we expect, we can mark this parent index as valid. + */ + inheritsRel = table_open(InheritsRelationId, AccessShareLock); + ScanKeyInit(&key, Anum_pg_inherits_inhparent, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(partedIdx))); + scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true, + NULL, 1, &key); + while ((inhTup = systable_getnext(scan)) != NULL) + { + Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup); + HeapTuple indTup; + Form_pg_index indexForm; + + indTup = SearchSysCache1(INDEXRELID, + ObjectIdGetDatum(inhForm->inhrelid)); + if (!HeapTupleIsValid(indTup)) + elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid); + indexForm = (Form_pg_index) GETSTRUCT(indTup); + if (indexForm->indisvalid) + tuples += 1; + ReleaseSysCache(indTup); + } + + /* Done with pg_inherits */ + systable_endscan(scan); + table_close(inheritsRel, AccessShareLock); + + /* + * If we found as many inherited indexes as the partitioned table has + * partitions, we're good; update pg_index to set indisvalid. + */ + if (tuples == RelationGetPartitionDesc(partedTbl, true)->nparts) + { + Relation idxRel; + HeapTuple indTup; + Form_pg_index indexForm; + + idxRel = table_open(IndexRelationId, RowExclusiveLock); + indTup = SearchSysCacheCopy1(INDEXRELID, + ObjectIdGetDatum(RelationGetRelid(partedIdx))); + if (!HeapTupleIsValid(indTup)) + elog(ERROR, "cache lookup failed for index %u", + RelationGetRelid(partedIdx)); + indexForm = (Form_pg_index) GETSTRUCT(indTup); + + indexForm->indisvalid = true; + updated = true; + + CatalogTupleUpdate(idxRel, &indTup->t_self, indTup); + + table_close(idxRel, RowExclusiveLock); + heap_freetuple(indTup); + } + + /* + * If this index is in turn a partition of a larger index, validating it + * might cause the parent to become valid also. Try that. + */ + if (updated && partedIdx->rd_rel->relispartition) + { + Oid parentIdxId, + parentTblId; + Relation parentIdx, + parentTbl; + + /* make sure we see the validation we just did */ + CommandCounterIncrement(); + + parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false); + parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false); + parentIdx = relation_open(parentIdxId, AccessExclusiveLock); + parentTbl = relation_open(parentTblId, AccessExclusiveLock); + Assert(!parentIdx->rd_index->indisvalid); + + validatePartitionedIndex(parentIdx, parentTbl); + + relation_close(parentIdx, AccessExclusiveLock); + relation_close(parentTbl, AccessExclusiveLock); + } +} + +/* + * ALTER INDEX i1 ATTACH PARTITION i2 + */ +ObjectAddress +ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) +{ + Relation partIdx; + Relation partTbl; + Relation parentTbl; + ObjectAddress address; + Oid partIdxId; + Oid currParent; + struct AttachIndexCallbackState state; + + /* + * We need to obtain lock on the index 'name' to modify it, but we also + * need to read its owning table's tuple descriptor -- so we need to lock + * both. To avoid deadlocks, obtain lock on the table before doing so on + * the index. Furthermore, we need to examine the parent table of the + * partition, so lock that one too. + */ + state.partitionOid = InvalidOid; + state.parentTblOid = parentIdx->rd_index->indrelid; + state.lockedParentTbl = false; + partIdxId = + RangeVarGetRelidExtended(name, AccessExclusiveLock, 0, + RangeVarCallbackForAttachIndex, + &state); + /* Not there? */ + if (!OidIsValid(partIdxId)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" does not exist", name->relname))); + + /* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */ + partIdx = relation_open(partIdxId, AccessExclusiveLock); + + /* we already hold locks on both tables, so this is safe: */ + parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock); + partTbl = relation_open(partIdx->rd_index->indrelid, NoLock); + + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx)); + + /* Silently do nothing if already in the right state */ + currParent = partIdx->rd_rel->relispartition ? + get_partition_parent(partIdxId, false) : InvalidOid; + if (currParent != RelationGetRelid(parentIdx)) + { + IndexInfo *childInfo; + IndexInfo *parentInfo; + AttrMap *attmap; + bool found; + int i; + PartitionDesc partDesc; + Oid constraintOid, + cldConstrId = InvalidOid; + + /* + * If this partition already has an index attached, refuse the + * operation. + */ + refuseDupeIndexAttach(parentIdx, partIdx, partTbl); + + if (OidIsValid(currParent)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentIdx)), + errdetail("Index \"%s\" is already attached to another index.", + RelationGetRelationName(partIdx)))); + + /* Make sure it indexes a partition of the other index's table */ + partDesc = RelationGetPartitionDesc(parentTbl, true); + found = false; + for (i = 0; i < partDesc->nparts; i++) + { + if (partDesc->oids[i] == state.partitionOid) + { + found = true; + break; + } + } + if (!found) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentIdx)), + errdetail("Index \"%s\" is not an index on any partition of table \"%s\".", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentTbl)))); + + /* Ensure the indexes are compatible */ + childInfo = BuildIndexInfo(partIdx); + parentInfo = BuildIndexInfo(parentIdx); + attmap = build_attrmap_by_name(RelationGetDescr(partTbl), + RelationGetDescr(parentTbl), + false); + if (!CompareIndexInfo(childInfo, parentInfo, + partIdx->rd_indcollation, + parentIdx->rd_indcollation, + partIdx->rd_opfamily, + parentIdx->rd_opfamily, + attmap)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentIdx)), + errdetail("The index definitions do not match."))); + + /* + * If there is a constraint in the parent, make sure there is one in + * the child too. + */ + constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(parentTbl), + RelationGetRelid(parentIdx)); + + if (OidIsValid(constraintOid)) + { + cldConstrId = get_relation_idx_constraint_oid(RelationGetRelid(partTbl), + partIdxId); + if (!OidIsValid(cldConstrId)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", + RelationGetRelationName(partIdx), + RelationGetRelationName(parentIdx)), + errdetail("The index \"%s\" belongs to a constraint in table \"%s\" but no constraint exists for index \"%s\".", + RelationGetRelationName(parentIdx), + RelationGetRelationName(parentTbl), + RelationGetRelationName(partIdx)))); + } + + /* + * If it's a primary key, make sure the columns in the partition are + * NOT NULL. + */ + if (parentIdx->rd_index->indisprimary) + verifyPartitionIndexNotNull(childInfo, partTbl); + + /* All good -- do it */ + IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); + if (OidIsValid(constraintOid)) + ConstraintSetParentConstraint(cldConstrId, constraintOid, + RelationGetRelid(partTbl)); + + free_attrmap(attmap); + + validatePartitionedIndex(parentIdx, parentTbl); + } + + relation_close(parentTbl, AccessShareLock); + /* keep these locks till commit */ + relation_close(partTbl, NoLock); + relation_close(partIdx, NoLock); + + return address; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 23ebaa3f230..c93d022aa6d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -61,6 +61,7 @@ #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/sequence.h" +#include "commands/partcmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" #include "commands/trigger.h" @@ -132,84 +133,6 @@ typedef struct OnCommitItem static List *on_commits = NIL; -/* - * State information for ALTER TABLE - * - * The pending-work queue for an ALTER TABLE is a List of AlteredTableInfo - * structs, one for each table modified by the operation (the named table - * plus any child tables that are affected). We save lists of subcommands - * to apply to this table (possibly modified by parse transformation steps); - * these lists will be executed in Phase 2. If a Phase 3 step is needed, - * necessary information is stored in the constraints and newvals lists. - * - * Phase 2 is divided into multiple passes; subcommands are executed in - * a pass determined by subcommand type. - */ - -typedef enum AlterTablePass -{ - AT_PASS_UNSET = -1, /* UNSET will cause ERROR */ - AT_PASS_DROP, /* DROP (all flavors) */ - AT_PASS_ALTER_TYPE, /* ALTER COLUMN TYPE */ - AT_PASS_ADD_COL, /* ADD COLUMN */ - AT_PASS_SET_EXPRESSION, /* ALTER SET EXPRESSION */ - AT_PASS_OLD_INDEX, /* re-add existing indexes */ - AT_PASS_OLD_CONSTR, /* re-add existing constraints */ - /* We could support a RENAME COLUMN pass here, but not currently used */ - AT_PASS_ADD_CONSTR, /* ADD constraints (initial examination) */ - AT_PASS_COL_ATTRS, /* set column attributes, eg NOT NULL */ - AT_PASS_ADD_INDEXCONSTR, /* ADD index-based constraints */ - AT_PASS_ADD_INDEX, /* ADD indexes */ - AT_PASS_ADD_OTHERCONSTR, /* ADD other constraints, defaults */ - AT_PASS_MISC, /* other stuff */ -} AlterTablePass; - -#define AT_NUM_PASSES (AT_PASS_MISC + 1) - -typedef struct AlteredTableInfo -{ - /* Information saved before any work commences: */ - Oid relid; /* Relation to work on */ - char relkind; /* Its relkind */ - TupleDesc oldDesc; /* Pre-modification tuple descriptor */ - - /* - * Transiently set during Phase 2, normally set to NULL. - * - * ATRewriteCatalogs sets this when it starts, and closes when ATExecCmd - * returns control. This can be exploited by ATExecCmd subroutines to - * close/reopen across transaction boundaries. - */ - Relation rel; - - /* Information saved by Phase 1 for Phase 2: */ - List *subcmds[AT_NUM_PASSES]; /* Lists of AlterTableCmd */ - /* Information saved by Phases 1/2 for Phase 3: */ - List *constraints; /* List of NewConstraint */ - List *newvals; /* List of NewColumnValue */ - List *afterStmts; /* List of utility command parsetrees */ - bool verify_new_notnull; /* T if we should recheck NOT NULL */ - int rewrite; /* Reason for forced rewrite, if any */ - bool chgAccessMethod; /* T if SET ACCESS METHOD is used */ - Oid newAccessMethod; /* new access method; 0 means no change, - * if above is true */ - Oid newTableSpace; /* new tablespace; 0 means no change */ - bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */ - char newrelpersistence; /* if above is true */ - Expr *partition_constraint; /* for attach partition validation */ - /* true, if validating default due to some other attach/detach */ - bool validate_default; - /* Objects to rebuild after completing ALTER TYPE operations */ - List *changedConstraintOids; /* OIDs of constraints to rebuild */ - List *changedConstraintDefs; /* string definitions of same */ - List *changedIndexOids; /* OIDs of indexes to rebuild */ - List *changedIndexDefs; /* string definitions of same */ - char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */ - char *clusterOnIndex; /* index to use for CLUSTER */ - List *changedStatisticsOids; /* OIDs of statistics to rebuild */ - List *changedStatisticsDefs; /* string definitions of same */ -} AlteredTableInfo; - /* Struct describing one new constraint to check in Phase 3 scan */ /* Note: new not-null constraints are handled elsewhere */ typedef struct NewConstraint @@ -325,17 +248,6 @@ struct DropRelationCallbackState char actual_relpersistence; }; -/* Alter table target-type flags for ATSimplePermissions */ -#define ATT_TABLE 0x0001 -#define ATT_VIEW 0x0002 -#define ATT_MATVIEW 0x0004 -#define ATT_INDEX 0x0008 -#define ATT_COMPOSITE_TYPE 0x0010 -#define ATT_FOREIGN_TABLE 0x0020 -#define ATT_PARTITIONED_INDEX 0x0040 -#define ATT_SEQUENCE 0x0080 -#define ATT_PARTITIONED_TABLE 0x0100 - /* * ForeignTruncateInfo * @@ -350,14 +262,6 @@ typedef struct ForeignTruncateInfo List *rels; } ForeignTruncateInfo; -/* Partial or complete FK creation in addFkConstraint() */ -typedef enum addFkConstraintSides -{ - addFkReferencedSide, - addFkReferencingSide, - addFkBothSides, -} addFkConstraintSides; - /* * Partition tables are expected to be dropped when the parent partitioned * table gets dropped. Hence for partitioning we use AUTO dependency. @@ -431,8 +335,6 @@ static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); -static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, - Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode); static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, char *constrName, HeapTuple contuple, bool recurse, bool recursing, LOCKMODE lockmode); @@ -476,8 +378,6 @@ static void ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, AlterTableUtilityContext *context); static void ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap); -static AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel); -static void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets); static void ATSimpleRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, bool recurse, LOCKMODE lockmode, AlterTableUtilityContext *context); @@ -508,8 +408,6 @@ static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, bool recurse, bool recursing, LOCKMODE lockmode); static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr); -static bool ConstraintImpliedByRelConstraint(Relation scanrel, - List *testConstraint, List *provenConstraint); static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName, Node *newDefault, LOCKMODE lockmode); static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum, @@ -518,8 +416,6 @@ static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName, Node *def, LOCKMODE lockmode, bool recurse, bool recursing); static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName, Node *def, LOCKMODE lockmode, bool recurse, bool recursing); -static ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode, - bool recurse, bool recursing); static ObjectAddress ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, Node *newExpr, LOCKMODE lockmode); static void ATPrepDropExpression(Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode); @@ -565,37 +461,6 @@ static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo * static int validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, int numfksetcols, int16 *fksetcolsattnums, List *fksetcols); -static ObjectAddress addFkConstraint(addFkConstraintSides fkside, - char *constraintname, - Constraint *fkconstraint, Relation rel, - Relation pkrel, Oid indexOid, - Oid parentConstr, - int numfks, int16 *pkattnum, int16 *fkattnum, - Oid *pfeqoperators, Oid *ppeqoperators, - Oid *ffeqoperators, int numfkdelsetcols, - int16 *fkdelsetcols, bool is_internal, - bool with_period); -static void addFkRecurseReferenced(Constraint *fkconstraint, - Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, - int numfks, int16 *pkattnum, int16 *fkattnum, - Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - int numfkdelsetcols, int16 *fkdelsetcols, - bool old_check_ok, - Oid parentDelTrigger, Oid parentUpdTrigger, - bool with_period); -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, - int numfkdelsetcols, int16 *fkdelsetcols, - bool old_check_ok, LOCKMODE lockmode, - Oid parentInsTrigger, Oid parentUpdTrigger, - bool with_period); -static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel, - Relation partitionRel); -static void CloneFkReferenced(Relation parentRel, Relation partitionRel); -static void CloneFkReferencing(List **wqueue, Relation parentRel, - Relation partRel); static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, Oid constraintOid, Oid indexOid, @@ -606,31 +471,8 @@ static void createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Oid indexOid, Oid parentDelTrigger, Oid parentUpdTrigger, Oid *deleteTrigOid, Oid *updateTrigOid); -static bool tryAttachPartitionForeignKey(List **wqueue, - ForeignKeyCacheInfo *fk, - Relation partition, - Oid parentConstrOid, int numfks, - AttrNumber *mapped_conkey, AttrNumber *confkey, - Oid *conpfeqop, - Oid parentInsTrigger, - Oid parentUpdTrigger, - Relation trigrel); -static void AttachPartitionForeignKey(List **wqueue, Relation partition, - Oid partConstrOid, Oid parentConstrOid, - Oid parentInsTrigger, Oid parentUpdTrigger, - Relation trigrel); -static void RemoveInheritedConstraint(Relation conrel, Relation trigrel, - Oid conoid, Oid conrelid); static void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid, Oid conrelid); -static void GetForeignKeyActionTriggers(Relation trigrel, - Oid conoid, Oid confrelid, Oid conrelid, - Oid *deleteTriggerOid, - Oid *updateTriggerOid); -static void GetForeignKeyCheckTriggers(Relation trigrel, - Oid conoid, Oid confrelid, Oid conrelid, - Oid *insertTriggerOid, - Oid *updateTriggerOid); static void ATExecDropConstraint(Relation rel, const char *constrName, DropBehavior behavior, bool recurse, bool missing_ok, LOCKMODE lockmode); @@ -707,36 +549,6 @@ static void RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid, void *arg); static void RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, void *arg); -static PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec); -static void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, - List **partexprs, Oid *partopclass, Oid *partcollation, - PartitionStrategy strategy); -static void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition); -static void RemoveInheritance(Relation child_rel, Relation parent_rel, - bool expect_detached); -static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel, - PartitionCmd *cmd, - AlterTableUtilityContext *context); -static void AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel); -static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, - List *partConstraint, - bool validate_default); -static void CloneRowTriggersToPartition(Relation parent, Relation partition); -static void DropClonedTriggersFromPartition(Oid partitionId); -static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, - Relation rel, RangeVar *name, - bool concurrent); -static void DetachPartitionFinalize(Relation rel, Relation partRel, - bool concurrent, Oid defaultPartOid); -static ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name); -static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, - RangeVar *name); -static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl); -static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, - Relation partitionTbl); -static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition); -static List *GetParentedForeignKeyRefs(Relation partition); -static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); static char GetAttributeStorage(Oid atttypid, const char *storagemode); @@ -6550,7 +6362,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) /* * ATGetQueueEntry: find or create an entry in the ALTER TABLE work queue */ -static AlteredTableInfo * +AlteredTableInfo * ATGetQueueEntry(List **wqueue, Relation rel) { Oid relid = RelationGetRelid(rel); @@ -6727,7 +6539,7 @@ alter_table_type_to_string(AlterTableType cmdtype) * - Ensure this user is the owner * - Ensure that it is not a system table */ -static void +void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets) { int actual_target; @@ -8476,7 +8288,7 @@ ATExecSetIdentity(Relation rel, const char *colName, Node *def, * * Return the address of the affected column. */ -static ObjectAddress +ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode, bool recurse, bool recursing) { @@ -10710,7 +10522,7 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums, * NULL/DEFAULT clause * with_period: true if this is a temporal FK */ -static ObjectAddress +ObjectAddress addFkConstraint(addFkConstraintSides fkside, char *constraintname, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, @@ -10888,7 +10700,7 @@ addFkConstraint(addFkConstraintSides fkside, * UPDATE respectively. * with_period: true if this is a temporal FK */ -static void +void addFkRecurseReferenced(Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, @@ -11026,7 +10838,7 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel, * UPDATE respectively. * with_period: true if this is a temporal FK */ -static void +void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, @@ -11192,795 +11004,6 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, } } -/* - * CloneForeignKeyConstraints - * Clone foreign keys from a partitioned table to a newly acquired - * partition. - * - * partitionRel is a partition of parentRel, so we can be certain that it has - * the same columns with the same datatypes. The columns may be in different - * order, though. - * - * wqueue must be passed to set up phase 3 constraint checking, unless the - * referencing-side partition is known to be empty (such as in CREATE TABLE / - * PARTITION OF). - */ -static void -CloneForeignKeyConstraints(List **wqueue, Relation parentRel, - Relation partitionRel) -{ - /* This only works for declarative partitioning */ - Assert(parentRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); - - /* - * First, clone constraints where the parent is on the referencing side. - */ - CloneFkReferencing(wqueue, parentRel, partitionRel); - - /* - * Clone constraints for which the parent is on the referenced side. - */ - CloneFkReferenced(parentRel, partitionRel); -} - -/* - * CloneFkReferenced - * Subroutine for CloneForeignKeyConstraints - * - * Find all the FKs that have the parent relation on the referenced side; - * clone those constraints to the given partition. This is to be called - * when the partition is being created or attached. - * - * This recurses to partitions, if the relation being attached is partitioned. - * Recursion is done by calling addFkRecurseReferenced. - */ -static void -CloneFkReferenced(Relation parentRel, Relation partitionRel) -{ - Relation pg_constraint; - AttrMap *attmap; - ListCell *cell; - SysScanDesc scan; - ScanKeyData key[2]; - HeapTuple tuple; - List *clone = NIL; - Relation trigrel; - - /* - * Search for any constraints where this partition's parent is in the - * referenced side. However, we must not clone any constraint whose - * parent constraint is also going to be cloned, to avoid duplicates. So - * do it in two steps: first construct the list of constraints to clone, - * then go over that list cloning those whose parents are not in the list. - * (We must not rely on the parent being seen first, since the catalog - * scan could return children first.) - */ - pg_constraint = table_open(ConstraintRelationId, RowShareLock); - ScanKeyInit(&key[0], - Anum_pg_constraint_confrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parentRel))); - ScanKeyInit(&key[1], - Anum_pg_constraint_contype, BTEqualStrategyNumber, - F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); - /* This is a seqscan, as we don't have a usable index ... */ - scan = systable_beginscan(pg_constraint, InvalidOid, true, - NULL, 2, key); - while ((tuple = systable_getnext(scan)) != NULL) - { - Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - clone = lappend_oid(clone, constrForm->oid); - } - systable_endscan(scan); - table_close(pg_constraint, RowShareLock); - - /* - * Triggers of the foreign keys will be manipulated a bunch of times in - * the loop below. To avoid repeatedly opening/closing the trigger - * catalog relation, we open it here and pass it to the subroutines called - * below. - */ - trigrel = table_open(TriggerRelationId, RowExclusiveLock); - - attmap = build_attrmap_by_name(RelationGetDescr(partitionRel), - RelationGetDescr(parentRel), - false); - foreach(cell, clone) - { - Oid constrOid = lfirst_oid(cell); - Form_pg_constraint constrForm; - Relation fkRel; - Oid indexOid; - Oid partIndexId; - int numfks; - AttrNumber conkey[INDEX_MAX_KEYS]; - AttrNumber mapped_confkey[INDEX_MAX_KEYS]; - AttrNumber confkey[INDEX_MAX_KEYS]; - Oid conpfeqop[INDEX_MAX_KEYS]; - Oid conppeqop[INDEX_MAX_KEYS]; - Oid conffeqop[INDEX_MAX_KEYS]; - int numfkdelsetcols; - AttrNumber confdelsetcols[INDEX_MAX_KEYS]; - Constraint *fkconstraint; - ObjectAddress address; - Oid deleteTriggerOid = InvalidOid, - updateTriggerOid = InvalidOid; - - tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for constraint %u", constrOid); - constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - /* - * As explained above: don't try to clone a constraint for which we're - * going to clone the parent. - */ - if (list_member_oid(clone, constrForm->conparentid)) - { - ReleaseSysCache(tuple); - continue; - } - - /* We need the same lock level that CreateTrigger will acquire */ - fkRel = table_open(constrForm->conrelid, ShareRowExclusiveLock); - - indexOid = constrForm->conindid; - DeconstructFkConstraintRow(tuple, - &numfks, - conkey, - confkey, - conpfeqop, - conppeqop, - conffeqop, - &numfkdelsetcols, - confdelsetcols); - - for (int i = 0; i < numfks; i++) - mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; - - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - fkconstraint->conname = NameStr(constrForm->conname); - fkconstraint->deferrable = constrForm->condeferrable; - fkconstraint->initdeferred = constrForm->condeferred; - fkconstraint->location = -1; - fkconstraint->pktable = NULL; - /* ->fk_attrs determined below */ - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = constrForm->confmatchtype; - fkconstraint->fk_upd_action = constrForm->confupdtype; - fkconstraint->fk_del_action = constrForm->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->is_enforced = constrForm->conenforced; - fkconstraint->skip_validation = false; - fkconstraint->initially_valid = constrForm->convalidated; - - /* set up colnames that are used to generate the constraint name */ - for (int i = 0; i < numfks; i++) - { - Form_pg_attribute att; - - att = TupleDescAttr(RelationGetDescr(fkRel), - conkey[i] - 1); - fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, - makeString(NameStr(att->attname))); - } - - /* - * Add the new foreign key constraint pointing to the new partition. - * Because this new partition appears in the referenced side of the - * constraint, we don't need to set up for Phase 3 check. - */ - partIndexId = index_get_partition(partitionRel, indexOid); - if (!OidIsValid(partIndexId)) - elog(ERROR, "index for %u not found in partition %s", - indexOid, RelationGetRelationName(partitionRel)); - - /* - * Get the "action" triggers belonging to the constraint to pass as - * parent OIDs for similar triggers that will be created on the - * partition in addFkRecurseReferenced(). - */ - if (constrForm->conenforced) - GetForeignKeyActionTriggers(trigrel, constrOid, - constrForm->confrelid, constrForm->conrelid, - &deleteTriggerOid, &updateTriggerOid); - - /* Add this constraint ... */ - address = addFkConstraint(addFkReferencedSide, - fkconstraint->conname, fkconstraint, fkRel, - partitionRel, partIndexId, constrOid, - numfks, mapped_confkey, - conkey, conpfeqop, conppeqop, conffeqop, - numfkdelsetcols, confdelsetcols, false, - constrForm->conperiod); - /* ... and recurse */ - addFkRecurseReferenced(fkconstraint, - fkRel, - partitionRel, - partIndexId, - address.objectId, - numfks, - mapped_confkey, - conkey, - conpfeqop, - conppeqop, - conffeqop, - numfkdelsetcols, - confdelsetcols, - true, - deleteTriggerOid, - updateTriggerOid, - constrForm->conperiod); - - table_close(fkRel, NoLock); - ReleaseSysCache(tuple); - } - - table_close(trigrel, RowExclusiveLock); -} - -/* - * CloneFkReferencing - * Subroutine for CloneForeignKeyConstraints - * - * For each FK constraint of the parent relation in the given list, find an - * equivalent constraint in its partition relation that can be reparented; - * if one cannot be found, create a new constraint in the partition as its - * child. - * - * If wqueue is given, it is used to set up phase-3 verification for each - * cloned constraint; omit it if such verification is not needed - * (example: the partition is being created anew). - */ -static void -CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) -{ - AttrMap *attmap; - List *partFKs; - List *clone = NIL; - ListCell *cell; - Relation trigrel; - - /* obtain a list of constraints that we need to clone */ - foreach(cell, RelationGetFKeyList(parentRel)) - { - ForeignKeyCacheInfo *fk = lfirst(cell); - - /* - * Refuse to attach a table as partition that this partitioned table - * already has a foreign key to. This isn't useful schema, which is - * proven by the fact that there have been no user complaints that - * it's already impossible to achieve this in the opposite direction, - * i.e., creating a foreign key that references a partition. This - * restriction allows us to dodge some complexities around - * pg_constraint and pg_trigger row creations that would be needed - * during ATTACH/DETACH for this kind of relationship. - */ - if (fk->confrelid == RelationGetRelid(partRel)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot attach table \"%s\" as a partition because it is referenced by foreign key \"%s\"", - RelationGetRelationName(partRel), - get_constraint_name(fk->conoid)))); - - clone = lappend_oid(clone, fk->conoid); - } - - /* - * Silently do nothing if there's nothing to do. In particular, this - * avoids throwing a spurious error for foreign tables. - */ - if (clone == NIL) - return; - - if (partRel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("foreign key constraints are not supported on foreign tables"))); - - /* - * Triggers of the foreign keys will be manipulated a bunch of times in - * the loop below. To avoid repeatedly opening/closing the trigger - * catalog relation, we open it here and pass it to the subroutines called - * below. - */ - trigrel = table_open(TriggerRelationId, RowExclusiveLock); - - /* - * The constraint key may differ, if the columns in the partition are - * different. This map is used to convert them. - */ - attmap = build_attrmap_by_name(RelationGetDescr(partRel), - RelationGetDescr(parentRel), - false); - - partFKs = copyObject(RelationGetFKeyList(partRel)); - - foreach(cell, clone) - { - Oid parentConstrOid = lfirst_oid(cell); - Form_pg_constraint constrForm; - Relation pkrel; - HeapTuple tuple; - int numfks; - AttrNumber conkey[INDEX_MAX_KEYS]; - AttrNumber mapped_conkey[INDEX_MAX_KEYS]; - AttrNumber confkey[INDEX_MAX_KEYS]; - Oid conpfeqop[INDEX_MAX_KEYS]; - Oid conppeqop[INDEX_MAX_KEYS]; - Oid conffeqop[INDEX_MAX_KEYS]; - int numfkdelsetcols; - AttrNumber confdelsetcols[INDEX_MAX_KEYS]; - Constraint *fkconstraint; - bool attached; - Oid indexOid; - ObjectAddress address; - ListCell *lc; - Oid insertTriggerOid = InvalidOid, - updateTriggerOid = InvalidOid; - bool with_period; - - tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(parentConstrOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for constraint %u", - parentConstrOid); - constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - /* Don't clone constraints whose parents are being cloned */ - if (list_member_oid(clone, constrForm->conparentid)) - { - ReleaseSysCache(tuple); - continue; - } - - /* - * Need to prevent concurrent deletions. If pkrel is a partitioned - * relation, that means to lock all partitions. - */ - pkrel = table_open(constrForm->confrelid, ShareRowExclusiveLock); - if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - (void) find_all_inheritors(RelationGetRelid(pkrel), - ShareRowExclusiveLock, NULL); - - DeconstructFkConstraintRow(tuple, &numfks, conkey, confkey, - conpfeqop, conppeqop, conffeqop, - &numfkdelsetcols, confdelsetcols); - for (int i = 0; i < numfks; i++) - mapped_conkey[i] = attmap->attnums[conkey[i] - 1]; - - /* - * Get the "check" triggers belonging to the constraint, if it is - * ENFORCED, to pass as parent OIDs for similar triggers that will be - * created on the partition in addFkRecurseReferencing(). They are - * also passed to tryAttachPartitionForeignKey() below to simply - * assign as parents to the partition's existing "check" triggers, - * that is, if the corresponding constraints is deemed attachable to - * the parent constraint. - */ - if (constrForm->conenforced) - GetForeignKeyCheckTriggers(trigrel, constrForm->oid, - constrForm->confrelid, constrForm->conrelid, - &insertTriggerOid, &updateTriggerOid); - - /* - * Before creating a new constraint, see whether any existing FKs are - * fit for the purpose. If one is, attach the parent constraint to - * it, and don't clone anything. This way we avoid the expensive - * verification step and don't end up with a duplicate FK, and we - * don't need to recurse to partitions for this constraint. - */ - attached = false; - foreach(lc, partFKs) - { - ForeignKeyCacheInfo *fk = lfirst_node(ForeignKeyCacheInfo, lc); - - if (tryAttachPartitionForeignKey(wqueue, - fk, - partRel, - parentConstrOid, - numfks, - mapped_conkey, - confkey, - conpfeqop, - insertTriggerOid, - updateTriggerOid, - trigrel)) - { - attached = true; - table_close(pkrel, NoLock); - break; - } - } - if (attached) - { - ReleaseSysCache(tuple); - continue; - } - - /* No dice. Set up to create our own constraint */ - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - /* ->conname determined below */ - fkconstraint->deferrable = constrForm->condeferrable; - fkconstraint->initdeferred = constrForm->condeferred; - fkconstraint->location = -1; - fkconstraint->pktable = NULL; - /* ->fk_attrs determined below */ - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = constrForm->confmatchtype; - fkconstraint->fk_upd_action = constrForm->confupdtype; - fkconstraint->fk_del_action = constrForm->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->is_enforced = constrForm->conenforced; - fkconstraint->skip_validation = false; - fkconstraint->initially_valid = constrForm->convalidated; - for (int i = 0; i < numfks; i++) - { - Form_pg_attribute att; - - att = TupleDescAttr(RelationGetDescr(partRel), - mapped_conkey[i] - 1); - fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, - makeString(NameStr(att->attname))); - } - - indexOid = constrForm->conindid; - with_period = constrForm->conperiod; - - /* Create the pg_constraint entry at this level */ - address = addFkConstraint(addFkReferencingSide, - NameStr(constrForm->conname), fkconstraint, - partRel, pkrel, indexOid, parentConstrOid, - numfks, confkey, - mapped_conkey, conpfeqop, - conppeqop, conffeqop, - numfkdelsetcols, confdelsetcols, - false, with_period); - - /* Done with the cloned constraint's tuple */ - ReleaseSysCache(tuple); - - /* Create the check triggers, and recurse to partitions, if any */ - addFkRecurseReferencing(wqueue, - fkconstraint, - partRel, - pkrel, - indexOid, - address.objectId, - numfks, - confkey, - mapped_conkey, - conpfeqop, - conppeqop, - conffeqop, - numfkdelsetcols, - confdelsetcols, - false, /* no old check exists */ - AccessExclusiveLock, - insertTriggerOid, - updateTriggerOid, - with_period); - table_close(pkrel, NoLock); - } - - table_close(trigrel, RowExclusiveLock); -} - -/* - * 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 - * partition might already have an equivalent foreign key; this routine - * compares the given ForeignKeyCacheInfo (in the partition) to the FK defined - * by the other parameters. If they are equivalent, create the link between - * the two constraints and return true. - * - * If the given FK does not match the one defined by rest of the params, - * return false. - */ -static bool -tryAttachPartitionForeignKey(List **wqueue, - ForeignKeyCacheInfo *fk, - Relation partition, - Oid parentConstrOid, - int numfks, - AttrNumber *mapped_conkey, - AttrNumber *confkey, - Oid *conpfeqop, - Oid parentInsTrigger, - Oid parentUpdTrigger, - Relation trigrel) -{ - HeapTuple parentConstrTup; - Form_pg_constraint parentConstr; - HeapTuple partcontup; - Form_pg_constraint partConstr; - - parentConstrTup = SearchSysCache1(CONSTROID, - ObjectIdGetDatum(parentConstrOid)); - if (!HeapTupleIsValid(parentConstrTup)) - elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid); - parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup); - - /* - * Do some quick & easy initial checks. If any of these fail, we cannot - * use this constraint. - */ - if (fk->confrelid != parentConstr->confrelid || fk->nkeys != numfks) - { - ReleaseSysCache(parentConstrTup); - return false; - } - for (int i = 0; i < numfks; i++) - { - if (fk->conkey[i] != mapped_conkey[i] || - fk->confkey[i] != confkey[i] || - fk->conpfeqop[i] != conpfeqop[i]) - { - ReleaseSysCache(parentConstrTup); - return false; - } - } - - /* Looks good so far; perform more extensive checks. */ - partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid)); - if (!HeapTupleIsValid(partcontup)) - elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); - partConstr = (Form_pg_constraint) GETSTRUCT(partcontup); - - /* - * An error should be raised if the constraint enforceability is - * different. Returning false without raising an error, as we do for other - * attributes, could lead to a duplicate constraint with the same - * enforceability as the parent. While this may be acceptable, it may not - * be ideal. Therefore, it's better to raise an error and allow the user - * to correct the enforceability before proceeding. - */ - if (partConstr->conenforced != parentConstr->conenforced) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("constraint \"%s\" enforceability conflicts with constraint \"%s\" on relation \"%s\"", - NameStr(parentConstr->conname), - NameStr(partConstr->conname), - RelationGetRelationName(partition)))); - - if (OidIsValid(partConstr->conparentid) || - partConstr->condeferrable != parentConstr->condeferrable || - partConstr->condeferred != parentConstr->condeferred || - partConstr->confupdtype != parentConstr->confupdtype || - partConstr->confdeltype != parentConstr->confdeltype || - partConstr->confmatchtype != parentConstr->confmatchtype) - { - ReleaseSysCache(parentConstrTup); - ReleaseSysCache(partcontup); - return false; - } - - ReleaseSysCache(parentConstrTup); - ReleaseSysCache(partcontup); - - /* Looks good! Attach this constraint. */ - AttachPartitionForeignKey(wqueue, partition, fk->conoid, - parentConstrOid, parentInsTrigger, - parentUpdTrigger, trigrel); - - return true; -} - -/* - * AttachPartitionForeignKey - * - * The subroutine for tryAttachPartitionForeignKey performs the final tasks of - * attaching the constraint, removing redundant triggers and entries from - * pg_constraint, and setting the constraint's parent. - */ -static void -AttachPartitionForeignKey(List **wqueue, - Relation partition, - Oid partConstrOid, - Oid parentConstrOid, - Oid parentInsTrigger, - Oid parentUpdTrigger, - Relation trigrel) -{ - HeapTuple parentConstrTup; - Form_pg_constraint parentConstr; - HeapTuple partcontup; - Form_pg_constraint partConstr; - bool queueValidation; - Oid partConstrFrelid; - Oid partConstrRelid; - bool parentConstrIsEnforced; - - /* Fetch the parent constraint tuple */ - parentConstrTup = SearchSysCache1(CONSTROID, - ObjectIdGetDatum(parentConstrOid)); - if (!HeapTupleIsValid(parentConstrTup)) - elog(ERROR, "cache lookup failed for constraint %u", parentConstrOid); - parentConstr = (Form_pg_constraint) GETSTRUCT(parentConstrTup); - parentConstrIsEnforced = parentConstr->conenforced; - - /* Fetch the child constraint tuple */ - partcontup = SearchSysCache1(CONSTROID, - ObjectIdGetDatum(partConstrOid)); - if (!HeapTupleIsValid(partcontup)) - elog(ERROR, "cache lookup failed for constraint %u", partConstrOid); - partConstr = (Form_pg_constraint) GETSTRUCT(partcontup); - partConstrFrelid = partConstr->confrelid; - partConstrRelid = partConstr->conrelid; - - /* - * If the referenced table is partitioned, then the partition we're - * attaching now has extra pg_constraint rows and action triggers that are - * no longer needed. Remove those. - */ - if (get_rel_relkind(partConstrFrelid) == RELKIND_PARTITIONED_TABLE) - { - Relation pg_constraint = table_open(ConstraintRelationId, RowShareLock); - - RemoveInheritedConstraint(pg_constraint, trigrel, partConstrOid, - partConstrRelid); - - table_close(pg_constraint, RowShareLock); - } - - /* - * Will we need to validate this constraint? A valid parent constraint - * implies that all child constraints have been validated, so if this one - * isn't, we must trigger phase 3 validation. - */ - queueValidation = parentConstr->convalidated && !partConstr->convalidated; - - ReleaseSysCache(partcontup); - ReleaseSysCache(parentConstrTup); - - /* - * The action triggers in the new partition become redundant -- the parent - * table already has equivalent ones, and those will be able to reach the - * partition. Remove the ones in the partition. We identify them because - * they have our constraint OID, as well as being on the referenced rel. - */ - DropForeignKeyConstraintTriggers(trigrel, partConstrOid, partConstrFrelid, - partConstrRelid); - - ConstraintSetParentConstraint(partConstrOid, parentConstrOid, - RelationGetRelid(partition)); - - /* - * Like the constraint, attach partition's "check" triggers to the - * corresponding parent triggers if the constraint is ENFORCED. NOT - * ENFORCED constraints do not have these triggers. - */ - if (parentConstrIsEnforced) - { - Oid insertTriggerOid, - updateTriggerOid; - - GetForeignKeyCheckTriggers(trigrel, - partConstrOid, partConstrFrelid, partConstrRelid, - &insertTriggerOid, &updateTriggerOid); - Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger)); - TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger, - RelationGetRelid(partition)); - Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger)); - TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger, - RelationGetRelid(partition)); - } - - /* - * We updated this pg_constraint row above to set its parent; validating - * it will cause its convalidated flag to change, so we need CCI here. In - * addition, we need it unconditionally for the rare case where the parent - * table has *two* identical constraints; when reaching this function for - * the second one, we must have made our changes visible, otherwise we - * would try to attach both to this one. - */ - CommandCounterIncrement(); - - /* If validation is needed, put it in the queue now. */ - if (queueValidation) - { - Relation conrel; - Oid confrelid; - - conrel = table_open(ConstraintRelationId, RowExclusiveLock); - - partcontup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(partConstrOid)); - if (!HeapTupleIsValid(partcontup)) - elog(ERROR, "cache lookup failed for constraint %u", partConstrOid); - - confrelid = ((Form_pg_constraint) GETSTRUCT(partcontup))->confrelid; - - /* Use the same lock as for AT_ValidateConstraint */ - QueueFKConstraintValidation(wqueue, conrel, partition, confrelid, - partcontup, ShareUpdateExclusiveLock); - ReleaseSysCache(partcontup); - table_close(conrel, RowExclusiveLock); - } -} - -/* - * RemoveInheritedConstraint - * - * Removes the constraint and its associated trigger from the specified - * relation, which inherited the given constraint. - */ -static void -RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid, - Oid conrelid) -{ - ObjectAddresses *objs; - HeapTuple consttup; - ScanKeyData key; - SysScanDesc scan; - HeapTuple trigtup; - - ScanKeyInit(&key, - Anum_pg_constraint_conrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conrelid)); - - scan = systable_beginscan(conrel, - ConstraintRelidTypidNameIndexId, - true, NULL, 1, &key); - objs = new_object_addresses(); - while ((consttup = systable_getnext(scan)) != NULL) - { - Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(consttup); - - if (conform->conparentid != conoid) - continue; - else - { - ObjectAddress addr; - SysScanDesc scan2; - ScanKeyData key2; - int n PG_USED_FOR_ASSERTS_ONLY; - - ObjectAddressSet(addr, ConstraintRelationId, conform->oid); - add_exact_object_address(&addr, objs); - - /* - * First we must delete the dependency record that binds the - * constraint records together. - */ - n = deleteDependencyRecordsForSpecific(ConstraintRelationId, - conform->oid, - DEPENDENCY_INTERNAL, - ConstraintRelationId, - conoid); - Assert(n == 1); /* actually only one is expected */ - - /* - * Now search for the triggers for this constraint and set them up - * for deletion too - */ - ScanKeyInit(&key2, - Anum_pg_trigger_tgconstraint, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conform->oid)); - scan2 = systable_beginscan(trigrel, TriggerConstraintIndexId, - true, NULL, 1, &key2); - while ((trigtup = systable_getnext(scan2)) != NULL) - { - ObjectAddressSet(addr, TriggerRelationId, - ((Form_pg_trigger) GETSTRUCT(trigtup))->oid); - add_exact_object_address(&addr, objs); - } - systable_endscan(scan2); - } - } - /* make the dependency deletions visible */ - CommandCounterIncrement(); - performMultipleDeletions(objs, DROP_RESTRICT, - PERFORM_DELETION_INTERNAL); - systable_endscan(scan); -} - /* * DropForeignKeyConstraintTriggers * @@ -12054,128 +11077,6 @@ DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid, systable_endscan(scan); } -/* - * GetForeignKeyActionTriggers - * Returns delete and update "action" triggers of the given relation - * belonging to the given constraint - */ -static void -GetForeignKeyActionTriggers(Relation trigrel, - Oid conoid, Oid confrelid, Oid conrelid, - Oid *deleteTriggerOid, - Oid *updateTriggerOid) -{ - ScanKeyData key; - SysScanDesc scan; - HeapTuple trigtup; - - *deleteTriggerOid = *updateTriggerOid = InvalidOid; - ScanKeyInit(&key, - Anum_pg_trigger_tgconstraint, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); - - scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true, - NULL, 1, &key); - while ((trigtup = systable_getnext(scan)) != NULL) - { - Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup); - - if (trgform->tgconstrrelid != conrelid) - continue; - if (trgform->tgrelid != confrelid) - continue; - /* Only ever look at "action" triggers on the PK side. */ - if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_PK) - continue; - if (TRIGGER_FOR_DELETE(trgform->tgtype)) - { - Assert(*deleteTriggerOid == InvalidOid); - *deleteTriggerOid = trgform->oid; - } - else if (TRIGGER_FOR_UPDATE(trgform->tgtype)) - { - Assert(*updateTriggerOid == InvalidOid); - *updateTriggerOid = trgform->oid; - } -#ifndef USE_ASSERT_CHECKING - /* In an assert-enabled build, continue looking to find duplicates */ - if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid)) - break; -#endif - } - - if (!OidIsValid(*deleteTriggerOid)) - elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u", - conoid); - if (!OidIsValid(*updateTriggerOid)) - elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u", - conoid); - - systable_endscan(scan); -} - -/* - * GetForeignKeyCheckTriggers - * Returns insert and update "check" triggers of the given relation - * belonging to the given constraint - */ -static void -GetForeignKeyCheckTriggers(Relation trigrel, - Oid conoid, Oid confrelid, Oid conrelid, - Oid *insertTriggerOid, - Oid *updateTriggerOid) -{ - ScanKeyData key; - SysScanDesc scan; - HeapTuple trigtup; - - *insertTriggerOid = *updateTriggerOid = InvalidOid; - ScanKeyInit(&key, - Anum_pg_trigger_tgconstraint, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(conoid)); - - scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true, - NULL, 1, &key); - while ((trigtup = systable_getnext(scan)) != NULL) - { - Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup); - - if (trgform->tgconstrrelid != confrelid) - continue; - if (trgform->tgrelid != conrelid) - continue; - /* Only ever look at "check" triggers on the FK side. */ - if (RI_FKey_trigger_type(trgform->tgfoid) != RI_TRIGGER_FK) - continue; - if (TRIGGER_FOR_INSERT(trgform->tgtype)) - { - Assert(*insertTriggerOid == InvalidOid); - *insertTriggerOid = trgform->oid; - } - else if (TRIGGER_FOR_UPDATE(trgform->tgtype)) - { - Assert(*updateTriggerOid == InvalidOid); - *updateTriggerOid = trgform->oid; - } -#ifndef USE_ASSERT_CHECKING - /* In an assert-enabled build, continue looking to find duplicates. */ - if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid)) - break; -#endif - } - - if (!OidIsValid(*insertTriggerOid)) - elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u", - conoid); - if (!OidIsValid(*updateTriggerOid)) - elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u", - conoid); - - systable_endscan(scan); -} - /* * ALTER TABLE ALTER CONSTRAINT * @@ -12986,7 +11887,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, * Phase 3 and update the convalidated field in the pg_constraint catalog * for the specified relation and all its children. */ -static void +void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) { @@ -17360,7 +16261,7 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) * * Common to ATExecAddInherit() and ATExecAttachPartition(). */ -static void +void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition) { Relation catalogRelation; @@ -17846,78 +16747,6 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) return address; } -/* - * MarkInheritDetached - * - * Set inhdetachpending for a partition, for ATExecDetachPartition - * in concurrent mode. While at it, verify that no other partition is - * already pending detach. - */ -static void -MarkInheritDetached(Relation child_rel, Relation parent_rel) -{ - Relation catalogRelation; - SysScanDesc scan; - ScanKeyData key; - HeapTuple inheritsTuple; - bool found = false; - - Assert(parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); - - /* - * Find pg_inherits entries by inhparent. (We need to scan them all in - * order to verify that no other partition is pending detach.) - */ - catalogRelation = table_open(InheritsRelationId, RowExclusiveLock); - ScanKeyInit(&key, - Anum_pg_inherits_inhparent, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(parent_rel))); - scan = systable_beginscan(catalogRelation, InheritsParentIndexId, - true, NULL, 1, &key); - - while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) - { - Form_pg_inherits inhForm; - - inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple); - if (inhForm->inhdetachpending) - ereport(ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("partition \"%s\" already pending detach in partitioned table \"%s.%s\"", - get_rel_name(inhForm->inhrelid), - get_namespace_name(parent_rel->rd_rel->relnamespace), - RelationGetRelationName(parent_rel)), - errhint("Use ALTER TABLE ... DETACH PARTITION ... FINALIZE to complete the pending detach operation.")); - - if (inhForm->inhrelid == RelationGetRelid(child_rel)) - { - HeapTuple newtup; - - newtup = heap_copytuple(inheritsTuple); - ((Form_pg_inherits) GETSTRUCT(newtup))->inhdetachpending = true; - - CatalogTupleUpdate(catalogRelation, - &inheritsTuple->t_self, - newtup); - found = true; - heap_freetuple(newtup); - /* keep looking, to ensure we catch others pending detach */ - } - } - - /* Done */ - systable_endscan(scan); - table_close(catalogRelation, RowExclusiveLock); - - if (!found) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_TABLE), - errmsg("relation \"%s\" is not a partition of relation \"%s\"", - RelationGetRelationName(child_rel), - RelationGetRelationName(parent_rel)))); -} - /* * RemoveInheritance * @@ -17936,7 +16765,7 @@ MarkInheritDetached(Relation child_rel, Relation parent_rel) * * Common to ATExecDropInherit() and ATExecDetachPartition(). */ -static void +void RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached) { Relation catalogRelation; @@ -19708,2271 +18537,6 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, ReleaseSysCache(tuple); } -/* - * Transform any expressions present in the partition key - * - * Returns a transformed PartitionSpec. - */ -static PartitionSpec * -transformPartitionSpec(Relation rel, PartitionSpec *partspec) -{ - PartitionSpec *newspec; - ParseState *pstate; - ParseNamespaceItem *nsitem; - ListCell *l; - - newspec = makeNode(PartitionSpec); - - newspec->strategy = partspec->strategy; - newspec->partParams = NIL; - newspec->location = partspec->location; - - /* Check valid number of columns for strategy */ - if (partspec->strategy == PARTITION_STRATEGY_LIST && - list_length(partspec->partParams) != 1) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use \"list\" partition strategy with more than one column"))); - - /* - * Create a dummy ParseState and insert the target relation as its sole - * rangetable entry. We need a ParseState for transformExpr. - */ - pstate = make_parsestate(NULL); - nsitem = addRangeTableEntryForRelation(pstate, rel, AccessShareLock, - NULL, false, true); - addNSItemToQuery(pstate, nsitem, true, true, true); - - /* take care of any partition expressions */ - foreach(l, partspec->partParams) - { - PartitionElem *pelem = lfirst_node(PartitionElem, l); - - if (pelem->expr) - { - /* Copy, to avoid scribbling on the input */ - pelem = copyObject(pelem); - - /* Now do parse transformation of the expression */ - pelem->expr = transformExpr(pstate, pelem->expr, - EXPR_KIND_PARTITION_EXPRESSION); - - /* we have to fix its collations too */ - assign_expr_collations(pstate, pelem->expr); - } - - newspec->partParams = lappend(newspec->partParams, pelem); - } - - return newspec; -} - -/* - * Compute per-partition-column information from a list of PartitionElems. - * Expressions in the PartitionElems must be parse-analyzed already. - */ -static void -ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, - List **partexprs, Oid *partopclass, Oid *partcollation, - PartitionStrategy strategy) -{ - int attn; - ListCell *lc; - Oid am_oid; - - attn = 0; - foreach(lc, partParams) - { - PartitionElem *pelem = lfirst_node(PartitionElem, lc); - Oid atttype; - Oid attcollation; - - if (pelem->name != NULL) - { - /* Simple attribute reference */ - HeapTuple atttuple; - Form_pg_attribute attform; - - atttuple = SearchSysCacheAttName(RelationGetRelid(rel), - pelem->name); - if (!HeapTupleIsValid(atttuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" named in partition key does not exist", - pelem->name), - parser_errposition(pstate, pelem->location))); - attform = (Form_pg_attribute) GETSTRUCT(atttuple); - - if (attform->attnum <= 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use system column \"%s\" in partition key", - pelem->name), - parser_errposition(pstate, pelem->location))); - - /* - * Stored generated columns cannot work: They are computed after - * BEFORE triggers, but partition routing is done before all - * triggers. Maybe virtual generated columns could be made to - * work, but then they would need to be handled as an expression - * below. - */ - if (attform->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use generated column in partition key"), - errdetail("Column \"%s\" is a generated column.", - pelem->name), - parser_errposition(pstate, pelem->location))); - - partattrs[attn] = attform->attnum; - atttype = attform->atttypid; - attcollation = attform->attcollation; - ReleaseSysCache(atttuple); - } - else - { - /* Expression */ - Node *expr = pelem->expr; - char partattname[16]; - Bitmapset *expr_attrs = NULL; - int i; - - Assert(expr != NULL); - atttype = exprType(expr); - attcollation = exprCollation(expr); - - /* - * The expression must be of a storable type (e.g., not RECORD). - * The test is the same as for whether a table column is of a safe - * type (which is why we needn't check for the non-expression - * case). - */ - snprintf(partattname, sizeof(partattname), "%d", attn + 1); - CheckAttributeType(partattname, - atttype, attcollation, - NIL, CHKATYPE_IS_PARTKEY); - - /* - * Strip any top-level COLLATE clause. This ensures that we treat - * "x COLLATE y" and "(x COLLATE y)" alike. - */ - while (IsA(expr, CollateExpr)) - expr = (Node *) ((CollateExpr *) expr)->arg; - - /* - * Examine all the columns in the partition key expression. When - * the whole-row reference is present, examine all the columns of - * the partitioned table. - */ - pull_varattnos(expr, 1, &expr_attrs); - if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs)) - { - expr_attrs = bms_add_range(expr_attrs, - 1 - FirstLowInvalidHeapAttributeNumber, - RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber); - expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber); - } - - i = -1; - while ((i = bms_next_member(expr_attrs, i)) >= 0) - { - AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; - - Assert(attno != 0); - - /* - * Cannot allow system column references, since that would - * make partition routing impossible: their values won't be - * known yet when we need to do that. - */ - if (attno < 0) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("partition key expressions cannot contain system column references"))); - - /* - * Stored generated columns cannot work: They are computed - * after BEFORE triggers, but partition routing is done before - * all triggers. Virtual generated columns could probably - * work, but it would require more work elsewhere (for example - * SET EXPRESSION would need to check whether the column is - * used in partition keys). Seems safer to prohibit for now. - */ - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use generated column in partition key"), - errdetail("Column \"%s\" is a generated column.", - get_attname(RelationGetRelid(rel), attno, false)), - parser_errposition(pstate, pelem->location))); - } - - if (IsA(expr, Var) && - ((Var *) expr)->varattno > 0) - { - - /* - * User wrote "(column)" or "(column COLLATE something)". - * Treat it like simple attribute anyway. - */ - partattrs[attn] = ((Var *) expr)->varattno; - } - else - { - partattrs[attn] = 0; /* marks the column as expression */ - *partexprs = lappend(*partexprs, expr); - - /* - * transformPartitionSpec() should have already rejected - * subqueries, aggregates, window functions, and SRFs, based - * on the EXPR_KIND_ for partition expressions. - */ - - /* - * Preprocess the expression before checking for mutability. - * This is essential for the reasons described in - * contain_mutable_functions_after_planning. However, we call - * expression_planner for ourselves rather than using that - * function, because if constant-folding reduces the - * expression to a constant, we'd like to know that so we can - * complain below. - * - * Like contain_mutable_functions_after_planning, assume that - * expression_planner won't scribble on its input, so this - * won't affect the partexprs entry we saved above. - */ - expr = (Node *) expression_planner((Expr *) expr); - - /* - * Partition expressions cannot contain mutable functions, - * because a given row must always map to the same partition - * as long as there is no change in the partition boundary - * structure. - */ - if (contain_mutable_functions(expr)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("functions in partition key expression must be marked IMMUTABLE"))); - - /* - * While it is not exactly *wrong* for a partition expression - * to be a constant, it seems better to reject such keys. - */ - if (IsA(expr, Const)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use constant expression as partition key"))); - } - } - - /* - * Apply collation override if any - */ - if (pelem->collation) - attcollation = get_collation_oid(pelem->collation, false); - - /* - * Check we have a collation iff it's a collatable type. The only - * expected failures here are (1) COLLATE applied to a noncollatable - * type, or (2) partition expression had an unresolved collation. But - * we might as well code this to be a complete consistency check. - */ - if (type_is_collatable(atttype)) - { - if (!OidIsValid(attcollation)) - ereport(ERROR, - (errcode(ERRCODE_INDETERMINATE_COLLATION), - errmsg("could not determine which collation to use for partition expression"), - errhint("Use the COLLATE clause to set the collation explicitly."))); - } - else - { - if (OidIsValid(attcollation)) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("collations are not supported by type %s", - format_type_be(atttype)))); - } - - partcollation[attn] = attcollation; - - /* - * Identify the appropriate operator class. For list and range - * partitioning, we use a btree operator class; hash partitioning uses - * a hash operator class. - */ - if (strategy == PARTITION_STRATEGY_HASH) - am_oid = HASH_AM_OID; - else - am_oid = BTREE_AM_OID; - - if (!pelem->opclass) - { - partopclass[attn] = GetDefaultOpClass(atttype, am_oid); - - if (!OidIsValid(partopclass[attn])) - { - if (strategy == PARTITION_STRATEGY_HASH) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("data type %s has no default operator class for access method \"%s\"", - format_type_be(atttype), "hash"), - errhint("You must specify a hash operator class or define a default hash operator class for the data type."))); - else - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("data type %s has no default operator class for access method \"%s\"", - format_type_be(atttype), "btree"), - errhint("You must specify a btree operator class or define a default btree operator class for the data type."))); - } - } - else - partopclass[attn] = ResolveOpClass(pelem->opclass, - atttype, - am_oid == HASH_AM_OID ? "hash" : "btree", - am_oid); - - attn++; - } -} - -/* - * PartConstraintImpliedByRelConstraint - * Do scanrel's existing constraints imply the partition constraint? - * - * "Existing constraints" include its check constraints and column-level - * not-null constraints. partConstraint describes the partition constraint, - * in implicit-AND form. - */ -bool -PartConstraintImpliedByRelConstraint(Relation scanrel, - List *partConstraint) -{ - List *existConstraint = NIL; - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int i; - - if (constr && constr->has_not_null) - { - int natts = scanrel->rd_att->natts; - - for (i = 1; i <= natts; i++) - { - CompactAttribute *att = TupleDescCompactAttr(scanrel->rd_att, i - 1); - - /* invalid not-null constraint must be ignored here */ - if (att->attnullability == ATTNULLABLE_VALID && !att->attisdropped) - { - Form_pg_attribute wholeatt = TupleDescAttr(scanrel->rd_att, i - 1); - NullTest *ntest = makeNode(NullTest); - - ntest->arg = (Expr *) makeVar(1, - i, - wholeatt->atttypid, - wholeatt->atttypmod, - wholeatt->attcollation, - 0); - ntest->nulltesttype = IS_NOT_NULL; - - /* - * argisrow=false is correct even for a composite column, - * because attnotnull does not represent a SQL-spec IS NOT - * NULL test in such a case, just IS DISTINCT FROM NULL. - */ - ntest->argisrow = false; - ntest->location = -1; - existConstraint = lappend(existConstraint, ntest); - } - } - } - - return ConstraintImpliedByRelConstraint(scanrel, partConstraint, existConstraint); -} - -/* - * ConstraintImpliedByRelConstraint - * Do scanrel's existing constraints imply the given constraint? - * - * testConstraint is the constraint to validate. provenConstraint is a - * caller-provided list of conditions which this function may assume - * to be true. Both provenConstraint and testConstraint must be in - * implicit-AND form, must only contain immutable clauses, and must - * contain only Vars with varno = 1. - */ -bool -ConstraintImpliedByRelConstraint(Relation scanrel, List *testConstraint, List *provenConstraint) -{ - List *existConstraint = list_copy(provenConstraint); - TupleConstr *constr = RelationGetDescr(scanrel)->constr; - int num_check, - i; - - num_check = (constr != NULL) ? constr->num_check : 0; - for (i = 0; i < num_check; i++) - { - Node *cexpr; - - /* - * If this constraint hasn't been fully validated yet, we must ignore - * it here. - */ - if (!constr->check[i].ccvalid) - continue; - - /* - * NOT ENFORCED constraints are always marked as invalid, which should - * have been ignored. - */ - Assert(constr->check[i].ccenforced); - - cexpr = stringToNode(constr->check[i].ccbin); - - /* - * Run each expression through const-simplification and - * canonicalization. It is necessary, because we will be comparing it - * to similarly-processed partition constraint expressions, and may - * fail to detect valid matches without this. - */ - cexpr = eval_const_expressions(NULL, cexpr); - cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true); - - existConstraint = list_concat(existConstraint, - make_ands_implicit((Expr *) cexpr)); - } - - /* - * Try to make the proof. Since we are comparing CHECK constraints, we - * need to use weak implication, i.e., we assume existConstraint is - * not-false and try to prove the same for testConstraint. - * - * Note that predicate_implied_by assumes its first argument is known - * immutable. That should always be true for both NOT NULL and partition - * constraints, so we don't test it here. - */ - return predicate_implied_by(testConstraint, existConstraint, true); -} - -/* - * QueuePartitionConstraintValidation - * - * Add an entry to wqueue to have the given partition constraint validated by - * Phase 3, for the given relation, and all its children. - * - * We first verify whether the given constraint is implied by pre-existing - * relation constraints; if it is, there's no need to scan the table to - * validate, so don't queue in that case. - */ -static void -QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, - List *partConstraint, - bool validate_default) -{ - /* - * Based on the table's existing constraints, determine whether or not we - * may skip scanning the table. - */ - if (PartConstraintImpliedByRelConstraint(scanrel, partConstraint)) - { - if (!validate_default) - ereport(DEBUG1, - (errmsg_internal("partition constraint for table \"%s\" is implied by existing constraints", - RelationGetRelationName(scanrel)))); - else - ereport(DEBUG1, - (errmsg_internal("updated partition constraint for default partition \"%s\" is implied by existing constraints", - RelationGetRelationName(scanrel)))); - return; - } - - /* - * Constraints proved insufficient. For plain relations, queue a - * validation item now; for partitioned tables, recurse to process each - * partition. - */ - if (scanrel->rd_rel->relkind == RELKIND_RELATION) - { - AlteredTableInfo *tab; - - /* Grab a work queue entry. */ - tab = ATGetQueueEntry(wqueue, scanrel); - Assert(tab->partition_constraint == NULL); - tab->partition_constraint = (Expr *) linitial(partConstraint); - tab->validate_default = validate_default; - } - else if (scanrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - PartitionDesc partdesc = RelationGetPartitionDesc(scanrel, true); - int i; - - for (i = 0; i < partdesc->nparts; i++) - { - Relation part_rel; - List *thisPartConstraint; - - /* - * This is the minimum lock we need to prevent deadlocks. - */ - part_rel = table_open(partdesc->oids[i], AccessExclusiveLock); - - /* - * Adjust the constraint for scanrel so that it matches this - * partition's attribute numbers. - */ - thisPartConstraint = - map_partition_varattnos(partConstraint, 1, - part_rel, scanrel); - - QueuePartitionConstraintValidation(wqueue, part_rel, - thisPartConstraint, - validate_default); - table_close(part_rel, NoLock); /* keep lock till commit */ - } - } -} - -/* - * ALTER TABLE ATTACH PARTITION FOR VALUES - * - * Return the address of the newly attached partition. - */ -static ObjectAddress -ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, - AlterTableUtilityContext *context) -{ - Relation attachrel, - catalog; - List *attachrel_children; - List *partConstraint; - SysScanDesc scan; - ScanKeyData skey; - AttrNumber attno; - int natts; - TupleDesc tupleDesc; - ObjectAddress address; - const char *trigger_name; - Oid defaultPartOid; - List *partBoundConstraint; - ParseState *pstate = make_parsestate(NULL); - - pstate->p_sourcetext = context->queryString; - - /* - * We must lock the default partition if one exists, because attaching a - * new partition will change its partition constraint. - */ - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - if (OidIsValid(defaultPartOid)) - LockRelationOid(defaultPartOid, AccessExclusiveLock); - - attachrel = table_openrv(cmd->name, AccessExclusiveLock); - - /* - * XXX I think it'd be a good idea to grab locks on all tables referenced - * by FKs at this point also. - */ - - /* - * Must be owner of both parent and source table -- parent was checked by - * ATSimplePermissions call in ATPrepCmd - */ - ATSimplePermissions(AT_AttachPartition, attachrel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); - - /* A partition can only have one parent */ - if (attachrel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is already a partition", - RelationGetRelationName(attachrel)))); - - if (OidIsValid(attachrel->rd_rel->reloftype)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a typed table as partition"))); - - /* - * Table being attached should not already be part of inheritance; either - * as a child table... - */ - catalog = table_open(InheritsRelationId, AccessShareLock); - ScanKeyInit(&skey, - Anum_pg_inherits_inhrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(attachrel))); - scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true, - NULL, 1, &skey); - if (HeapTupleIsValid(systable_getnext(scan))) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach inheritance child as partition"))); - systable_endscan(scan); - - /* ...or as a parent table (except the case when it is partitioned) */ - ScanKeyInit(&skey, - Anum_pg_inherits_inhparent, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(attachrel))); - scan = systable_beginscan(catalog, InheritsParentIndexId, true, NULL, - 1, &skey); - if (HeapTupleIsValid(systable_getnext(scan)) && - attachrel->rd_rel->relkind == RELKIND_RELATION) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach inheritance parent as partition"))); - systable_endscan(scan); - table_close(catalog, AccessShareLock); - - /* - * Prevent circularity by seeing if rel is a partition of attachrel. (In - * particular, this disallows making a rel a partition of itself.) - * - * We do that by checking if rel is a member of the list of attachrel's - * partitions provided the latter is partitioned at all. We want to avoid - * having to construct this list again, so we request the strongest lock - * on all partitions. We need the strongest lock, because we may decide - * to scan them if we find out that the table being attached (or its leaf - * partitions) may contain rows that violate the partition constraint. If - * the table has a constraint that would prevent such rows, which by - * definition is present in all the partitions, we need not scan the - * table, nor its partitions. But we cannot risk a deadlock by taking a - * weaker lock now and the stronger one only when needed. - */ - attachrel_children = find_all_inheritors(RelationGetRelid(attachrel), - AccessExclusiveLock, NULL); - if (list_member_oid(attachrel_children, RelationGetRelid(rel))) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("circular inheritance not allowed"), - errdetail("\"%s\" is already a child of \"%s\".", - RelationGetRelationName(rel), - RelationGetRelationName(attachrel)))); - - /* If the parent is permanent, so must be all of its partitions. */ - if (rel->rd_rel->relpersistence != RELPERSISTENCE_TEMP && - attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a temporary relation as partition of permanent relation \"%s\"", - RelationGetRelationName(rel)))); - - /* Temp parent cannot have a partition that is itself not a temp */ - if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - attachrel->rd_rel->relpersistence != RELPERSISTENCE_TEMP) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach a permanent relation as partition of temporary relation \"%s\"", - RelationGetRelationName(rel)))); - - /* If the parent is temp, it must belong to this session */ - if (RELATION_IS_OTHER_TEMP(rel)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach as partition of temporary relation of another session"))); - - /* Ditto for the partition */ - if (RELATION_IS_OTHER_TEMP(attachrel)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach temporary relation of another session as partition"))); - - /* - * Check if attachrel has any identity columns or any columns that aren't - * in the parent. - */ - tupleDesc = RelationGetDescr(attachrel); - natts = tupleDesc->natts; - for (attno = 1; attno <= natts; attno++) - { - Form_pg_attribute attribute = TupleDescAttr(tupleDesc, attno - 1); - char *attributeName = NameStr(attribute->attname); - - /* Ignore dropped */ - if (attribute->attisdropped) - continue; - - if (attribute->attidentity) - ereport(ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("table \"%s\" being attached contains an identity column \"%s\"", - RelationGetRelationName(attachrel), attributeName), - errdetail("The new partition may not contain an identity column.")); - - /* Try to find the column in parent (matching on column name) */ - if (!SearchSysCacheExists2(ATTNAME, - ObjectIdGetDatum(RelationGetRelid(rel)), - CStringGetDatum(attributeName))) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("table \"%s\" contains column \"%s\" not found in parent \"%s\"", - RelationGetRelationName(attachrel), attributeName, - RelationGetRelationName(rel)), - errdetail("The new partition may contain only the columns present in parent."))); - } - - /* - * If child_rel has row-level triggers with transition tables, we - * currently don't allow it to become a partition. See also prohibitions - * in ATExecAddInherit() and CreateTrigger(). - */ - trigger_name = FindTriggerIncompatibleWithInheritance(attachrel->trigdesc); - if (trigger_name != NULL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition", - trigger_name, RelationGetRelationName(attachrel)), - errdetail("ROW triggers with transition tables are not supported on partitions."))); - - /* - * Check that the new partition's bound is valid and does not overlap any - * of existing partitions of the parent - note that it does not return on - * error. - */ - check_new_partition_bound(RelationGetRelationName(attachrel), rel, - cmd->bound, pstate); - - /* OK to create inheritance. Rest of the checks performed there */ - CreateInheritance(attachrel, rel, true); - - /* Update the pg_class entry. */ - StorePartitionBound(attachrel, rel, cmd->bound); - - /* Ensure there exists a correct set of indexes in the partition. */ - AttachPartitionEnsureIndexes(wqueue, rel, attachrel); - - /* and triggers */ - CloneRowTriggersToPartition(rel, attachrel); - - /* - * Clone foreign key constraints. Callee is responsible for setting up - * for phase 3 constraint verification. - */ - CloneForeignKeyConstraints(wqueue, rel, attachrel); - - /* - * Generate partition constraint from the partition bound specification. - * If the parent itself is a partition, make sure to include its - * constraint as well. - */ - partBoundConstraint = get_qual_from_partbound(rel, cmd->bound); - - /* - * Use list_concat_copy() to avoid modifying partBoundConstraint in place, - * since it's needed later to construct the constraint expression for - * validating against the default partition, if any. - */ - partConstraint = list_concat_copy(partBoundConstraint, - RelationGetPartitionQual(rel)); - - /* Skip validation if there are no constraints to validate. */ - if (partConstraint) - { - /* - * Run the partition quals through const-simplification similar to - * check constraints. We skip canonicalize_qual, though, because - * partition quals should be in canonical form already. - */ - partConstraint = - (List *) eval_const_expressions(NULL, - (Node *) partConstraint); - - /* XXX this sure looks wrong */ - partConstraint = list_make1(make_ands_explicit(partConstraint)); - - /* - * Adjust the generated constraint to match this partition's attribute - * numbers. - */ - partConstraint = map_partition_varattnos(partConstraint, 1, attachrel, - rel); - - /* Validate partition constraints against the table being attached. */ - QueuePartitionConstraintValidation(wqueue, attachrel, partConstraint, - false); - } - - /* - * If we're attaching a partition other than the default partition and a - * default one exists, then that partition's partition constraint changes, - * so add an entry to the work queue to validate it, too. (We must not do - * this when the partition being attached is the default one; we already - * did it above!) - */ - if (OidIsValid(defaultPartOid)) - { - Relation defaultrel; - List *defPartConstraint; - - Assert(!cmd->bound->is_default); - - /* we already hold a lock on the default partition */ - defaultrel = table_open(defaultPartOid, NoLock); - defPartConstraint = - get_proposed_default_constraint(partBoundConstraint); - - /* - * Map the Vars in the constraint expression from rel's attnos to - * defaultrel's. - */ - defPartConstraint = - map_partition_varattnos(defPartConstraint, - 1, defaultrel, rel); - QueuePartitionConstraintValidation(wqueue, defaultrel, - defPartConstraint, true); - - /* keep our lock until commit. */ - table_close(defaultrel, NoLock); - } - - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(attachrel)); - - /* - * If the partition we just attached is partitioned itself, invalidate - * relcache for all descendent partitions too to ensure that their - * rd_partcheck expression trees are rebuilt; partitions already locked at - * the beginning of this function. - */ - if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - ListCell *l; - - foreach(l, attachrel_children) - { - CacheInvalidateRelcacheByRelid(lfirst_oid(l)); - } - } - - /* keep our lock until commit */ - table_close(attachrel, NoLock); - - return address; -} - -/* - * AttachPartitionEnsureIndexes - * subroutine for ATExecAttachPartition to create/match indexes - * - * Enforce the indexing rule for partitioned tables during ALTER TABLE / ATTACH - * PARTITION: every partition must have an index attached to each index on the - * partitioned table. - */ -static void -AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) -{ - List *idxes; - List *attachRelIdxs; - Relation *attachrelIdxRels; - IndexInfo **attachInfos; - ListCell *cell; - MemoryContext cxt; - MemoryContext oldcxt; - - cxt = AllocSetContextCreate(CurrentMemoryContext, - "AttachPartitionEnsureIndexes", - ALLOCSET_DEFAULT_SIZES); - oldcxt = MemoryContextSwitchTo(cxt); - - idxes = RelationGetIndexList(rel); - attachRelIdxs = RelationGetIndexList(attachrel); - attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs)); - attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs)); - - /* Build arrays of all existing indexes and their IndexInfos */ - foreach_oid(cldIdxId, attachRelIdxs) - { - int i = foreach_current_index(cldIdxId); - - attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock); - attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]); - } - - /* - * If we're attaching a foreign table, we must fail if any of the indexes - * is a constraint index; otherwise, there's nothing to do here. Do this - * before starting work, to avoid wasting the effort of building a few - * non-unique indexes before coming across a unique one. - */ - if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) - { - foreach(cell, idxes) - { - Oid idx = lfirst_oid(cell); - Relation idxRel = index_open(idx, AccessShareLock); - - if (idxRel->rd_index->indisunique || - idxRel->rd_index->indisprimary) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot attach foreign table \"%s\" as partition of partitioned table \"%s\"", - RelationGetRelationName(attachrel), - RelationGetRelationName(rel)), - errdetail("Partitioned table \"%s\" contains unique indexes.", - RelationGetRelationName(rel)))); - index_close(idxRel, AccessShareLock); - } - - goto out; - } - - /* - * For each index on the partitioned table, find a matching one in the - * partition-to-be; if one is not found, create one. - */ - foreach(cell, idxes) - { - Oid idx = lfirst_oid(cell); - Relation idxRel = index_open(idx, AccessShareLock); - IndexInfo *info; - AttrMap *attmap; - bool found = false; - Oid constraintOid; - - /* - * Ignore indexes in the partitioned table other than partitioned - * indexes. - */ - if (idxRel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) - { - index_close(idxRel, AccessShareLock); - continue; - } - - /* construct an indexinfo to compare existing indexes against */ - info = BuildIndexInfo(idxRel); - attmap = build_attrmap_by_name(RelationGetDescr(attachrel), - RelationGetDescr(rel), - false); - constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), idx); - - /* - * Scan the list of existing indexes in the partition-to-be, and mark - * the first matching, valid, unattached one we find, if any, as - * partition of the parent index. If we find one, we're done. - */ - for (int i = 0; i < list_length(attachRelIdxs); i++) - { - Oid cldIdxId = RelationGetRelid(attachrelIdxRels[i]); - Oid cldConstrOid = InvalidOid; - - /* does this index have a parent? if so, can't use it */ - if (attachrelIdxRels[i]->rd_rel->relispartition) - continue; - - /* If this index is invalid, can't use it */ - if (!attachrelIdxRels[i]->rd_index->indisvalid) - continue; - - if (CompareIndexInfo(attachInfos[i], info, - attachrelIdxRels[i]->rd_indcollation, - idxRel->rd_indcollation, - attachrelIdxRels[i]->rd_opfamily, - idxRel->rd_opfamily, - attmap)) - { - /* - * If this index is being created in the parent because of a - * constraint, then the child needs to have a constraint also, - * so look for one. If there is no such constraint, this - * index is no good, so keep looking. - */ - if (OidIsValid(constraintOid)) - { - cldConstrOid = - get_relation_idx_constraint_oid(RelationGetRelid(attachrel), - cldIdxId); - /* no dice */ - if (!OidIsValid(cldConstrOid)) - continue; - - /* Ensure they're both the same type of constraint */ - if (get_constraint_type(constraintOid) != - get_constraint_type(cldConstrOid)) - continue; - } - - /* bingo. */ - IndexSetParentIndex(attachrelIdxRels[i], idx); - if (OidIsValid(constraintOid)) - ConstraintSetParentConstraint(cldConstrOid, constraintOid, - RelationGetRelid(attachrel)); - found = true; - - CommandCounterIncrement(); - break; - } - } - - /* - * If no suitable index was found in the partition-to-be, create one - * now. Note that if this is a PK, not-null constraints must already - * exist. - */ - if (!found) - { - IndexStmt *stmt; - Oid conOid; - - stmt = generateClonedIndexStmt(NULL, - idxRel, attmap, - &conOid); - DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid, - RelationGetRelid(idxRel), - conOid, - -1, - true, false, false, false, false); - } - - index_close(idxRel, AccessShareLock); - } - -out: - /* Clean up. */ - for (int i = 0; i < list_length(attachRelIdxs); i++) - index_close(attachrelIdxRels[i], AccessShareLock); - MemoryContextSwitchTo(oldcxt); - MemoryContextDelete(cxt); -} - -/* - * CloneRowTriggersToPartition - * subroutine for ATExecAttachPartition/DefineRelation to create row - * triggers on partitions - */ -static void -CloneRowTriggersToPartition(Relation parent, Relation partition) -{ - Relation pg_trigger; - ScanKeyData key; - SysScanDesc scan; - HeapTuple tuple; - MemoryContext perTupCxt; - - ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent))); - pg_trigger = table_open(TriggerRelationId, RowExclusiveLock); - scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId, - true, NULL, 1, &key); - - perTupCxt = AllocSetContextCreate(CurrentMemoryContext, - "clone trig", ALLOCSET_SMALL_SIZES); - - while (HeapTupleIsValid(tuple = systable_getnext(scan))) - { - Form_pg_trigger trigForm = (Form_pg_trigger) GETSTRUCT(tuple); - CreateTrigStmt *trigStmt; - Node *qual = NULL; - Datum value; - bool isnull; - List *cols = NIL; - List *trigargs = NIL; - MemoryContext oldcxt; - - /* - * Ignore statement-level triggers; those are not cloned. - */ - if (!TRIGGER_FOR_ROW(trigForm->tgtype)) - continue; - - /* - * Don't clone internal triggers, because the constraint cloning code - * will. - */ - if (trigForm->tgisinternal) - continue; - - /* - * Complain if we find an unexpected trigger type. - */ - if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) && - !TRIGGER_FOR_AFTER(trigForm->tgtype)) - elog(ERROR, "unexpected trigger \"%s\" found", - NameStr(trigForm->tgname)); - - /* Use short-lived context for CREATE TRIGGER */ - oldcxt = MemoryContextSwitchTo(perTupCxt); - - /* - * If there is a WHEN clause, generate a 'cooked' version of it that's - * appropriate for the partition. - */ - value = heap_getattr(tuple, Anum_pg_trigger_tgqual, - RelationGetDescr(pg_trigger), &isnull); - if (!isnull) - { - qual = stringToNode(TextDatumGetCString(value)); - qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO, - partition, parent); - qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO, - partition, parent); - } - - /* - * If there is a column list, transform it to a list of column names. - * Note we don't need to map this list in any way ... - */ - if (trigForm->tgattr.dim1 > 0) - { - int i; - - for (i = 0; i < trigForm->tgattr.dim1; i++) - { - Form_pg_attribute col; - - col = TupleDescAttr(parent->rd_att, - trigForm->tgattr.values[i] - 1); - cols = lappend(cols, - makeString(pstrdup(NameStr(col->attname)))); - } - } - - /* Reconstruct trigger arguments list. */ - if (trigForm->tgnargs > 0) - { - char *p; - - value = heap_getattr(tuple, Anum_pg_trigger_tgargs, - RelationGetDescr(pg_trigger), &isnull); - if (isnull) - elog(ERROR, "tgargs is null for trigger \"%s\" in partition \"%s\"", - NameStr(trigForm->tgname), RelationGetRelationName(partition)); - - p = (char *) VARDATA_ANY(DatumGetByteaPP(value)); - - for (int i = 0; i < trigForm->tgnargs; i++) - { - trigargs = lappend(trigargs, makeString(pstrdup(p))); - p += strlen(p) + 1; - } - } - - trigStmt = makeNode(CreateTrigStmt); - trigStmt->replace = false; - trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint); - trigStmt->trigname = NameStr(trigForm->tgname); - trigStmt->relation = NULL; - trigStmt->funcname = NULL; /* passed separately */ - trigStmt->args = trigargs; - trigStmt->row = true; - trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK; - trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK; - trigStmt->columns = cols; - trigStmt->whenClause = NULL; /* passed separately */ - trigStmt->transitionRels = NIL; /* not supported at present */ - trigStmt->deferrable = trigForm->tgdeferrable; - trigStmt->initdeferred = trigForm->tginitdeferred; - trigStmt->constrrel = NULL; /* passed separately */ - - CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition), - trigForm->tgconstrrelid, InvalidOid, InvalidOid, - trigForm->tgfoid, trigForm->oid, qual, - false, true, trigForm->tgenabled); - - MemoryContextSwitchTo(oldcxt); - MemoryContextReset(perTupCxt); - } - - MemoryContextDelete(perTupCxt); - - systable_endscan(scan); - table_close(pg_trigger, RowExclusiveLock); -} - -/* - * ALTER TABLE DETACH PARTITION - * - * Return the address of the relation that is no longer a partition of rel. - * - * If concurrent mode is requested, we run in two transactions. A side- - * effect is that this command cannot run in a multi-part ALTER TABLE. - * Currently, that's enforced by the grammar. - * - * The strategy for concurrency is to first modify the partition's - * pg_inherit catalog row to make it visible to everyone that the - * partition is detached, lock the partition against writes, and commit - * the transaction; anyone who requests the partition descriptor from - * that point onwards has to ignore such a partition. In a second - * transaction, we wait until all transactions that could have seen the - * partition as attached are gone, then we remove the rest of partition - * metadata (pg_inherits and pg_class.relpartbounds). - */ -static ObjectAddress -ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, - RangeVar *name, bool concurrent) -{ - Relation partRel; - ObjectAddress address; - Oid defaultPartOid; - - /* - * We must lock the default partition, because detaching this partition - * will change its partition constraint. - */ - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - if (OidIsValid(defaultPartOid)) - { - /* - * Concurrent detaching when a default partition exists is not - * supported. The main problem is that the default partition - * constraint would change. And there's a definitional problem: what - * should happen to the tuples that are being inserted that belong to - * the partition being detached? Putting them on the partition being - * detached would be wrong, since they'd become "lost" after the - * detaching completes but we cannot put them in the default partition - * either until we alter its partition constraint. - * - * I think we could solve this problem if we effected the constraint - * change before committing the first transaction. But the lock would - * have to remain AEL and it would cause concurrent query planning to - * be blocked, so changing it that way would be even worse. - */ - if (concurrent) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot detach partitions concurrently when a default partition exists"))); - LockRelationOid(defaultPartOid, AccessExclusiveLock); - } - - /* - * In concurrent mode, the partition is locked with share-update-exclusive - * in the first transaction. This allows concurrent transactions to be - * doing DML to the partition. - */ - partRel = table_openrv(name, concurrent ? ShareUpdateExclusiveLock : - AccessExclusiveLock); - - /* - * Check inheritance conditions and either delete the pg_inherits row (in - * non-concurrent mode) or just set the inhdetachpending flag. - */ - if (!concurrent) - RemoveInheritance(partRel, rel, false); - else - MarkInheritDetached(partRel, rel); - - /* - * Ensure that foreign keys still hold after this detach. This keeps - * locks on the referencing tables, which prevents concurrent transactions - * from adding rows that we wouldn't see. For this to work in concurrent - * mode, it is critical that the partition appears as no longer attached - * for the RI queries as soon as the first transaction commits. - */ - ATDetachCheckNoForeignKeyRefs(partRel); - - /* - * Concurrent mode has to work harder; first we add a new constraint to - * the partition that matches the partition constraint. Then we close our - * existing transaction, and in a new one wait for all processes to catch - * up on the catalog updates we've done so far; at that point we can - * complete the operation. - */ - if (concurrent) - { - Oid partrelid, - parentrelid; - LOCKTAG tag; - char *parentrelname; - char *partrelname; - - /* - * We're almost done now; the only traces that remain are the - * pg_inherits tuple and the partition's relpartbounds. Before we can - * remove those, we need to wait until all transactions that know that - * this is a partition are gone. - */ - - /* - * Remember relation OIDs to re-acquire them later; and relation names - * too, for error messages if something is dropped in between. - */ - partrelid = RelationGetRelid(partRel); - parentrelid = RelationGetRelid(rel); - parentrelname = MemoryContextStrdup(PortalContext, - RelationGetRelationName(rel)); - partrelname = MemoryContextStrdup(PortalContext, - RelationGetRelationName(partRel)); - - /* Invalidate relcache entries for the parent -- must be before close */ - CacheInvalidateRelcache(rel); - - table_close(partRel, NoLock); - table_close(rel, NoLock); - tab->rel = NULL; - - /* Make updated catalog entry visible */ - PopActiveSnapshot(); - CommitTransactionCommand(); - - StartTransactionCommand(); - - /* - * Now wait. This ensures that all queries that were planned - * including the partition are finished before we remove the rest of - * catalog entries. We don't need or indeed want to acquire this - * lock, though -- that would block later queries. - * - * We don't need to concern ourselves with waiting for a lock on the - * partition itself, since we will acquire AccessExclusiveLock below. - */ - SET_LOCKTAG_RELATION(tag, MyDatabaseId, parentrelid); - WaitForLockersMultiple(list_make1(&tag), AccessExclusiveLock, false); - - /* - * Now acquire locks in both relations again. Note they may have been - * removed in the meantime, so care is required. - */ - rel = try_relation_open(parentrelid, ShareUpdateExclusiveLock); - partRel = try_relation_open(partrelid, AccessExclusiveLock); - - /* If the relations aren't there, something bad happened; bail out */ - if (rel == NULL) - { - if (partRel != NULL) /* shouldn't happen */ - elog(WARNING, "dangling partition \"%s\" remains, can't fix", - partrelname); - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("partitioned table \"%s\" was removed concurrently", - parentrelname))); - } - if (partRel == NULL) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("partition \"%s\" was removed concurrently", partrelname))); - - tab->rel = rel; - } - - /* - * Detaching the partition might involve TOAST table access, so ensure we - * have a valid snapshot. - */ - PushActiveSnapshot(GetTransactionSnapshot()); - - /* Do the final part of detaching */ - DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid); - - PopActiveSnapshot(); - - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); - - /* keep our lock until commit */ - table_close(partRel, NoLock); - - return address; -} - -/* - * Second part of ALTER TABLE .. DETACH. - * - * This is separate so that it can be run independently when the second - * transaction of the concurrent algorithm fails (crash or abort). - */ -static void -DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent, - Oid defaultPartOid) -{ - Relation classRel; - List *fks; - ListCell *cell; - List *indexes; - Datum new_val[Natts_pg_class]; - bool new_null[Natts_pg_class], - new_repl[Natts_pg_class]; - HeapTuple tuple, - newtuple; - Relation trigrel = NULL; - List *fkoids = NIL; - - if (concurrent) - { - /* - * We can remove the pg_inherits row now. (In the non-concurrent case, - * this was already done). - */ - RemoveInheritance(partRel, rel, true); - } - - /* Drop any triggers that were cloned on creation/attach. */ - DropClonedTriggersFromPartition(RelationGetRelid(partRel)); - - /* - * Detach any foreign keys that are inherited. This includes creating - * additional action triggers. - */ - fks = copyObject(RelationGetFKeyList(partRel)); - if (fks != NIL) - trigrel = table_open(TriggerRelationId, RowExclusiveLock); - - /* - * It's possible that the partition being detached has a foreign key that - * references a partitioned table. When that happens, there are multiple - * pg_constraint rows for the partition: one points to the partitioned - * table itself, while the others point to each of its partitions. Only - * the topmost one is to be considered here; the child constraints must be - * left alone, because conceptually those aren't coming from our parent - * partitioned table, but from this partition itself. - * - * We implement this by collecting all the constraint OIDs in a first scan - * of the FK array, and skipping in the loop below those constraints whose - * parents are listed here. - */ - foreach_node(ForeignKeyCacheInfo, fk, fks) - fkoids = lappend_oid(fkoids, fk->conoid); - - foreach(cell, fks) - { - ForeignKeyCacheInfo *fk = lfirst(cell); - HeapTuple contup; - Form_pg_constraint conform; - - contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid)); - if (!HeapTupleIsValid(contup)) - elog(ERROR, "cache lookup failed for constraint %u", fk->conoid); - conform = (Form_pg_constraint) GETSTRUCT(contup); - - /* - * Consider only inherited foreign keys, and only if their parents - * aren't in the list. - */ - if (conform->contype != CONSTRAINT_FOREIGN || - !OidIsValid(conform->conparentid) || - list_member_oid(fkoids, conform->conparentid)) - { - ReleaseSysCache(contup); - continue; - } - - /* - * The constraint on this table must be marked no longer a child of - * the parent's constraint, as do its check triggers. - */ - ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid); - - /* - * Also, look up the partition's "check" triggers corresponding to the - * ENFORCED constraint being detached and detach them from the parent - * triggers. NOT ENFORCED constraints do not have these triggers; - * therefore, this step is not needed. - */ - if (fk->conenforced) - { - Oid insertTriggerOid, - updateTriggerOid; - - GetForeignKeyCheckTriggers(trigrel, - fk->conoid, fk->confrelid, fk->conrelid, - &insertTriggerOid, &updateTriggerOid); - Assert(OidIsValid(insertTriggerOid)); - TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, - RelationGetRelid(partRel)); - Assert(OidIsValid(updateTriggerOid)); - TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, - RelationGetRelid(partRel)); - } - - /* - * Lastly, create the action triggers on the referenced table, using - * addFkRecurseReferenced, which requires some elaborate setup (so put - * it in a separate block). While at it, if the table is partitioned, - * that function will recurse to create the pg_constraint rows and - * action triggers for each partition. - * - * Note there's no need to do addFkConstraint() here, because the - * pg_constraint row already exists. - */ - { - Constraint *fkconstraint; - int numfks; - AttrNumber conkey[INDEX_MAX_KEYS]; - AttrNumber confkey[INDEX_MAX_KEYS]; - Oid conpfeqop[INDEX_MAX_KEYS]; - Oid conppeqop[INDEX_MAX_KEYS]; - Oid conffeqop[INDEX_MAX_KEYS]; - int numfkdelsetcols; - AttrNumber confdelsetcols[INDEX_MAX_KEYS]; - Relation refdRel; - - DeconstructFkConstraintRow(contup, - &numfks, - conkey, - confkey, - conpfeqop, - conppeqop, - conffeqop, - &numfkdelsetcols, - confdelsetcols); - - /* Create a synthetic node we'll use throughout */ - fkconstraint = makeNode(Constraint); - fkconstraint->contype = CONSTRAINT_FOREIGN; - fkconstraint->conname = pstrdup(NameStr(conform->conname)); - fkconstraint->deferrable = conform->condeferrable; - fkconstraint->initdeferred = conform->condeferred; - fkconstraint->is_enforced = conform->conenforced; - fkconstraint->skip_validation = true; - fkconstraint->initially_valid = conform->convalidated; - /* a few irrelevant fields omitted here */ - fkconstraint->pktable = NULL; - fkconstraint->fk_attrs = NIL; - fkconstraint->pk_attrs = NIL; - fkconstraint->fk_matchtype = conform->confmatchtype; - fkconstraint->fk_upd_action = conform->confupdtype; - fkconstraint->fk_del_action = conform->confdeltype; - fkconstraint->fk_del_set_cols = NIL; - fkconstraint->old_conpfeqop = NIL; - fkconstraint->old_pktable_oid = InvalidOid; - fkconstraint->location = -1; - - /* set up colnames, used to generate the constraint name */ - for (int i = 0; i < numfks; i++) - { - Form_pg_attribute att; - - att = TupleDescAttr(RelationGetDescr(partRel), - conkey[i] - 1); - - fkconstraint->fk_attrs = lappend(fkconstraint->fk_attrs, - makeString(NameStr(att->attname))); - } - - refdRel = table_open(fk->confrelid, ShareRowExclusiveLock); - - addFkRecurseReferenced(fkconstraint, partRel, - refdRel, - conform->conindid, - fk->conoid, - numfks, - confkey, - conkey, - conpfeqop, - conppeqop, - conffeqop, - numfkdelsetcols, - confdelsetcols, - true, - InvalidOid, InvalidOid, - conform->conperiod); - table_close(refdRel, NoLock); /* keep lock till end of xact */ - } - - ReleaseSysCache(contup); - } - list_free_deep(fks); - if (trigrel) - table_close(trigrel, RowExclusiveLock); - - /* - * Any sub-constraints that are in the referenced-side of a larger - * constraint have to be removed. This partition is no longer part of the - * key space of the constraint. - */ - foreach(cell, GetParentedForeignKeyRefs(partRel)) - { - Oid constrOid = lfirst_oid(cell); - ObjectAddress constraint; - - ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); - deleteDependencyRecordsForClass(ConstraintRelationId, - constrOid, - ConstraintRelationId, - DEPENDENCY_INTERNAL); - CommandCounterIncrement(); - - ObjectAddressSet(constraint, ConstraintRelationId, constrOid); - performDeletion(&constraint, DROP_RESTRICT, 0); - } - - /* Now we can detach indexes */ - indexes = RelationGetIndexList(partRel); - foreach(cell, indexes) - { - Oid idxid = lfirst_oid(cell); - Oid parentidx; - Relation idx; - Oid constrOid; - Oid parentConstrOid; - - if (!has_superclass(idxid)) - continue; - - parentidx = get_partition_parent(idxid, false); - Assert((IndexGetRelation(parentidx, false) == RelationGetRelid(rel))); - - idx = index_open(idxid, AccessExclusiveLock); - IndexSetParentIndex(idx, InvalidOid); - - /* - * If there's a constraint associated with the index, detach it too. - * Careful: it is possible for a constraint index in a partition to be - * the child of a non-constraint index, so verify whether the parent - * index does actually have a constraint. - */ - constrOid = get_relation_idx_constraint_oid(RelationGetRelid(partRel), - idxid); - parentConstrOid = get_relation_idx_constraint_oid(RelationGetRelid(rel), - parentidx); - if (OidIsValid(parentConstrOid) && OidIsValid(constrOid)) - ConstraintSetParentConstraint(constrOid, InvalidOid, InvalidOid); - - index_close(idx, NoLock); - } - - /* Update pg_class tuple */ - classRel = table_open(RelationRelationId, RowExclusiveLock); - tuple = SearchSysCacheCopy1(RELOID, - ObjectIdGetDatum(RelationGetRelid(partRel))); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", - RelationGetRelid(partRel)); - Assert(((Form_pg_class) GETSTRUCT(tuple))->relispartition); - - /* Clear relpartbound and reset relispartition */ - memset(new_val, 0, sizeof(new_val)); - memset(new_null, false, sizeof(new_null)); - memset(new_repl, false, sizeof(new_repl)); - new_val[Anum_pg_class_relpartbound - 1] = (Datum) 0; - new_null[Anum_pg_class_relpartbound - 1] = true; - new_repl[Anum_pg_class_relpartbound - 1] = true; - newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel), - new_val, new_null, new_repl); - - ((Form_pg_class) GETSTRUCT(newtuple))->relispartition = false; - CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple); - heap_freetuple(newtuple); - table_close(classRel, RowExclusiveLock); - - /* - * Drop identity property from all identity columns of partition. - */ - for (int attno = 0; attno < RelationGetNumberOfAttributes(partRel); attno++) - { - Form_pg_attribute attr = TupleDescAttr(partRel->rd_att, attno); - - if (!attr->attisdropped && attr->attidentity) - ATExecDropIdentity(partRel, NameStr(attr->attname), false, - AccessExclusiveLock, true, true); - } - - if (OidIsValid(defaultPartOid)) - { - /* - * If the relation being detached is the default partition itself, - * remove it from the parent's pg_partitioned_table entry. - * - * If not, we must invalidate default partition's relcache entry, as - * in StorePartitionBound: its partition constraint depends on every - * other partition's partition constraint. - */ - if (RelationGetRelid(partRel) == defaultPartOid) - update_default_partition_oid(RelationGetRelid(rel), InvalidOid); - else - CacheInvalidateRelcacheByRelid(defaultPartOid); - } - - /* - * Invalidate the parent's relcache so that the partition is no longer - * included in its partition descriptor. - */ - CacheInvalidateRelcache(rel); - - /* - * If the partition we just detached is partitioned itself, invalidate - * relcache for all descendent partitions too to ensure that their - * rd_partcheck expression trees are rebuilt; must lock partitions before - * doing so, using the same lockmode as what partRel has been locked with - * by the caller. - */ - if (partRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - List *children; - - children = find_all_inheritors(RelationGetRelid(partRel), - AccessExclusiveLock, NULL); - foreach(cell, children) - { - CacheInvalidateRelcacheByRelid(lfirst_oid(cell)); - } - } -} - -/* - * ALTER TABLE ... DETACH PARTITION ... FINALIZE - * - * To use when a DETACH PARTITION command previously did not run to - * completion; this completes the detaching process. - */ -static ObjectAddress -ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) -{ - Relation partRel; - ObjectAddress address; - Snapshot snap = GetActiveSnapshot(); - - partRel = table_openrv(name, AccessExclusiveLock); - - /* - * Wait until existing snapshots are gone. This is important if the - * second transaction of DETACH PARTITION CONCURRENTLY is canceled: the - * user could immediately run DETACH FINALIZE without actually waiting for - * existing transactions. We must not complete the detach action until - * all such queries are complete (otherwise we would present them with an - * inconsistent view of catalogs). - */ - WaitForOlderSnapshots(snap->xmin, false); - - DetachPartitionFinalize(rel, partRel, true, InvalidOid); - - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); - - table_close(partRel, NoLock); - - return address; -} - -/* - * DropClonedTriggersFromPartition - * subroutine for ATExecDetachPartition to remove any triggers that were - * cloned to the partition when it was created-as-partition or attached. - * This undoes what CloneRowTriggersToPartition did. - */ -static void -DropClonedTriggersFromPartition(Oid partitionId) -{ - ScanKeyData skey; - SysScanDesc scan; - HeapTuple trigtup; - Relation tgrel; - ObjectAddresses *objects; - - objects = new_object_addresses(); - - /* - * Scan pg_trigger to search for all triggers on this rel. - */ - ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(partitionId)); - tgrel = table_open(TriggerRelationId, RowExclusiveLock); - scan = systable_beginscan(tgrel, TriggerRelidNameIndexId, - true, NULL, 1, &skey); - while (HeapTupleIsValid(trigtup = systable_getnext(scan))) - { - Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup); - ObjectAddress trig; - - /* Ignore triggers that weren't cloned */ - if (!OidIsValid(pg_trigger->tgparentid)) - continue; - - /* - * Ignore internal triggers that are implementation objects of foreign - * keys, because these will be detached when the foreign keys - * themselves are. - */ - if (OidIsValid(pg_trigger->tgconstrrelid)) - continue; - - /* - * This is ugly, but necessary: remove the dependency markings on the - * trigger so that it can be removed. - */ - deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, - TriggerRelationId, - DEPENDENCY_PARTITION_PRI); - deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid, - RelationRelationId, - DEPENDENCY_PARTITION_SEC); - - /* remember this trigger to remove it below */ - ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid); - add_exact_object_address(&trig, objects); - } - - /* make the dependency removal visible to the deletion below */ - CommandCounterIncrement(); - performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); - - /* done */ - free_object_addresses(objects); - systable_endscan(scan); - table_close(tgrel, RowExclusiveLock); -} - -/* - * Before acquiring lock on an index, acquire the same lock on the owning - * table. - */ -struct AttachIndexCallbackState -{ - Oid partitionOid; - Oid parentTblOid; - bool lockedParentTbl; -}; - -static void -RangeVarCallbackForAttachIndex(const RangeVar *rv, Oid relOid, Oid oldRelOid, - void *arg) -{ - struct AttachIndexCallbackState *state; - Form_pg_class classform; - HeapTuple tuple; - - state = (struct AttachIndexCallbackState *) arg; - - if (!state->lockedParentTbl) - { - LockRelationOid(state->parentTblOid, AccessShareLock); - state->lockedParentTbl = true; - } - - /* - * If we previously locked some other heap, and the name we're looking up - * no longer refers to an index on that relation, release the now-useless - * lock. XXX maybe we should do *after* we verify whether the index does - * not actually belong to the same relation ... - */ - if (relOid != oldRelOid && OidIsValid(state->partitionOid)) - { - UnlockRelationOid(state->partitionOid, AccessShareLock); - state->partitionOid = InvalidOid; - } - - /* Didn't find a relation, so no need for locking or permission checks. */ - if (!OidIsValid(relOid)) - return; - - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); - if (!HeapTupleIsValid(tuple)) - return; /* concurrently dropped, so nothing to do */ - classform = (Form_pg_class) GETSTRUCT(tuple); - if (classform->relkind != RELKIND_PARTITIONED_INDEX && - classform->relkind != RELKIND_INDEX) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("\"%s\" is not an index", rv->relname))); - ReleaseSysCache(tuple); - - /* - * Since we need only examine the heap's tupledesc, an access share lock - * on it (preventing any DDL) is sufficient. - */ - state->partitionOid = IndexGetRelation(relOid, false); - LockRelationOid(state->partitionOid, AccessShareLock); -} - -/* - * ALTER INDEX i1 ATTACH PARTITION i2 - */ -static ObjectAddress -ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) -{ - Relation partIdx; - Relation partTbl; - Relation parentTbl; - ObjectAddress address; - Oid partIdxId; - Oid currParent; - struct AttachIndexCallbackState state; - - /* - * We need to obtain lock on the index 'name' to modify it, but we also - * need to read its owning table's tuple descriptor -- so we need to lock - * both. To avoid deadlocks, obtain lock on the table before doing so on - * the index. Furthermore, we need to examine the parent table of the - * partition, so lock that one too. - */ - state.partitionOid = InvalidOid; - state.parentTblOid = parentIdx->rd_index->indrelid; - state.lockedParentTbl = false; - partIdxId = - RangeVarGetRelidExtended(name, AccessExclusiveLock, 0, - RangeVarCallbackForAttachIndex, - &state); - /* Not there? */ - if (!OidIsValid(partIdxId)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" does not exist", name->relname))); - - /* no deadlock risk: RangeVarGetRelidExtended already acquired the lock */ - partIdx = relation_open(partIdxId, AccessExclusiveLock); - - /* we already hold locks on both tables, so this is safe: */ - parentTbl = relation_open(parentIdx->rd_index->indrelid, AccessShareLock); - partTbl = relation_open(partIdx->rd_index->indrelid, NoLock); - - ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx)); - - /* Silently do nothing if already in the right state */ - currParent = partIdx->rd_rel->relispartition ? - get_partition_parent(partIdxId, false) : InvalidOid; - if (currParent != RelationGetRelid(parentIdx)) - { - IndexInfo *childInfo; - IndexInfo *parentInfo; - AttrMap *attmap; - bool found; - int i; - PartitionDesc partDesc; - Oid constraintOid, - cldConstrId = InvalidOid; - - /* - * If this partition already has an index attached, refuse the - * operation. - */ - refuseDupeIndexAttach(parentIdx, partIdx, partTbl); - - if (OidIsValid(currParent)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Index \"%s\" is already attached to another index.", - RelationGetRelationName(partIdx)))); - - /* Make sure it indexes a partition of the other index's table */ - partDesc = RelationGetPartitionDesc(parentTbl, true); - found = false; - for (i = 0; i < partDesc->nparts; i++) - { - if (partDesc->oids[i] == state.partitionOid) - { - found = true; - break; - } - } - if (!found) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Index \"%s\" is not an index on any partition of table \"%s\".", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentTbl)))); - - /* Ensure the indexes are compatible */ - childInfo = BuildIndexInfo(partIdx); - parentInfo = BuildIndexInfo(parentIdx); - attmap = build_attrmap_by_name(RelationGetDescr(partTbl), - RelationGetDescr(parentTbl), - false); - if (!CompareIndexInfo(childInfo, parentInfo, - partIdx->rd_indcollation, - parentIdx->rd_indcollation, - partIdx->rd_opfamily, - parentIdx->rd_opfamily, - attmap)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("The index definitions do not match."))); - - /* - * If there is a constraint in the parent, make sure there is one in - * the child too. - */ - constraintOid = get_relation_idx_constraint_oid(RelationGetRelid(parentTbl), - RelationGetRelid(parentIdx)); - - if (OidIsValid(constraintOid)) - { - cldConstrId = get_relation_idx_constraint_oid(RelationGetRelid(partTbl), - partIdxId); - if (!OidIsValid(cldConstrId)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("The index \"%s\" belongs to a constraint in table \"%s\" but no constraint exists for index \"%s\".", - RelationGetRelationName(parentIdx), - RelationGetRelationName(parentTbl), - RelationGetRelationName(partIdx)))); - } - - /* - * If it's a primary key, make sure the columns in the partition are - * NOT NULL. - */ - if (parentIdx->rd_index->indisprimary) - verifyPartitionIndexNotNull(childInfo, partTbl); - - /* All good -- do it */ - IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx)); - if (OidIsValid(constraintOid)) - ConstraintSetParentConstraint(cldConstrId, constraintOid, - RelationGetRelid(partTbl)); - - free_attrmap(attmap); - - validatePartitionedIndex(parentIdx, parentTbl); - } - - relation_close(parentTbl, AccessShareLock); - /* keep these locks till commit */ - relation_close(partTbl, NoLock); - relation_close(partIdx, NoLock); - - return address; -} - -/* - * Verify whether the given partition already contains an index attached - * to the given partitioned index. If so, raise an error. - */ -static void -refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTbl) -{ - Oid existingIdx; - - existingIdx = index_get_partition(partitionTbl, - RelationGetRelid(parentIdx)); - if (OidIsValid(existingIdx)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", - RelationGetRelationName(partIdx), - RelationGetRelationName(parentIdx)), - errdetail("Another index \"%s\" is already attached for partition \"%s\".", - get_rel_name(existingIdx), - RelationGetRelationName(partitionTbl)))); -} - -/* - * Verify whether the set of attached partition indexes to a parent index on - * a partitioned table is complete. If it is, mark the parent index valid. - * - * This should be called each time a partition index is attached. - */ -static void -validatePartitionedIndex(Relation partedIdx, Relation partedTbl) -{ - Relation inheritsRel; - SysScanDesc scan; - ScanKeyData key; - int tuples = 0; - HeapTuple inhTup; - bool updated = false; - - Assert(partedIdx->rd_rel->relkind == RELKIND_PARTITIONED_INDEX); - - /* - * Scan pg_inherits for this parent index. Count each valid index we find - * (verifying the pg_index entry for each), and if we reach the total - * amount we expect, we can mark this parent index as valid. - */ - inheritsRel = table_open(InheritsRelationId, AccessShareLock); - ScanKeyInit(&key, Anum_pg_inherits_inhparent, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(RelationGetRelid(partedIdx))); - scan = systable_beginscan(inheritsRel, InheritsParentIndexId, true, - NULL, 1, &key); - while ((inhTup = systable_getnext(scan)) != NULL) - { - Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inhTup); - HeapTuple indTup; - Form_pg_index indexForm; - - indTup = SearchSysCache1(INDEXRELID, - ObjectIdGetDatum(inhForm->inhrelid)); - if (!HeapTupleIsValid(indTup)) - elog(ERROR, "cache lookup failed for index %u", inhForm->inhrelid); - indexForm = (Form_pg_index) GETSTRUCT(indTup); - if (indexForm->indisvalid) - tuples += 1; - ReleaseSysCache(indTup); - } - - /* Done with pg_inherits */ - systable_endscan(scan); - table_close(inheritsRel, AccessShareLock); - - /* - * If we found as many inherited indexes as the partitioned table has - * partitions, we're good; update pg_index to set indisvalid. - */ - if (tuples == RelationGetPartitionDesc(partedTbl, true)->nparts) - { - Relation idxRel; - HeapTuple indTup; - Form_pg_index indexForm; - - idxRel = table_open(IndexRelationId, RowExclusiveLock); - indTup = SearchSysCacheCopy1(INDEXRELID, - ObjectIdGetDatum(RelationGetRelid(partedIdx))); - if (!HeapTupleIsValid(indTup)) - elog(ERROR, "cache lookup failed for index %u", - RelationGetRelid(partedIdx)); - indexForm = (Form_pg_index) GETSTRUCT(indTup); - - indexForm->indisvalid = true; - updated = true; - - CatalogTupleUpdate(idxRel, &indTup->t_self, indTup); - - table_close(idxRel, RowExclusiveLock); - heap_freetuple(indTup); - } - - /* - * If this index is in turn a partition of a larger index, validating it - * might cause the parent to become valid also. Try that. - */ - if (updated && partedIdx->rd_rel->relispartition) - { - Oid parentIdxId, - parentTblId; - Relation parentIdx, - parentTbl; - - /* make sure we see the validation we just did */ - CommandCounterIncrement(); - - parentIdxId = get_partition_parent(RelationGetRelid(partedIdx), false); - parentTblId = get_partition_parent(RelationGetRelid(partedTbl), false); - parentIdx = relation_open(parentIdxId, AccessExclusiveLock); - parentTbl = relation_open(parentTblId, AccessExclusiveLock); - Assert(!parentIdx->rd_index->indisvalid); - - validatePartitionedIndex(parentIdx, parentTbl); - - relation_close(parentIdx, AccessExclusiveLock); - relation_close(parentTbl, AccessExclusiveLock); - } -} - -/* - * When attaching an index as a partition of a partitioned index which is a - * primary key, verify that all the columns in the partition are marked NOT - * NULL. - */ -static void -verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition) -{ - for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++) - { - Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition), - iinfo->ii_IndexAttrNumbers[i] - 1); - - if (!att->attnotnull) - ereport(ERROR, - errcode(ERRCODE_INVALID_TABLE_DEFINITION), - errmsg("invalid primary key definition"), - errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.", - NameStr(att->attname), - RelationGetRelationName(partition))); - } -} - -/* - * Return an OID list of constraints that reference the given relation - * that are marked as having a parent constraints. - */ -static List * -GetParentedForeignKeyRefs(Relation partition) -{ - Relation pg_constraint; - HeapTuple tuple; - SysScanDesc scan; - ScanKeyData key[2]; - List *constraints = NIL; - - /* - * If no indexes, or no columns are referenceable by FKs, we can avoid the - * scan. - */ - if (RelationGetIndexList(partition) == NIL || - bms_is_empty(RelationGetIndexAttrBitmap(partition, - INDEX_ATTR_BITMAP_KEY))) - return NIL; - - /* Search for constraints referencing this table */ - pg_constraint = table_open(ConstraintRelationId, AccessShareLock); - ScanKeyInit(&key[0], - Anum_pg_constraint_confrelid, BTEqualStrategyNumber, - F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partition))); - ScanKeyInit(&key[1], - Anum_pg_constraint_contype, BTEqualStrategyNumber, - F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); - - /* XXX This is a seqscan, as we don't have a usable index */ - scan = systable_beginscan(pg_constraint, InvalidOid, true, NULL, 2, key); - while ((tuple = systable_getnext(scan)) != NULL) - { - Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - /* - * We only need to process constraints that are part of larger ones. - */ - if (!OidIsValid(constrForm->conparentid)) - continue; - - constraints = lappend_oid(constraints, constrForm->oid); - } - - systable_endscan(scan); - table_close(pg_constraint, AccessShareLock); - - return constraints; -} - -/* - * During DETACH PARTITION, verify that any foreign keys pointing to the - * partitioned table would not become invalid. An error is raised if any - * referenced values exist. - */ -static void -ATDetachCheckNoForeignKeyRefs(Relation partition) -{ - List *constraints; - ListCell *cell; - - constraints = GetParentedForeignKeyRefs(partition); - - foreach(cell, constraints) - { - Oid constrOid = lfirst_oid(cell); - HeapTuple tuple; - Form_pg_constraint constrForm; - Relation rel; - Trigger trig = {0}; - - tuple = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constrOid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for constraint %u", constrOid); - constrForm = (Form_pg_constraint) GETSTRUCT(tuple); - - Assert(OidIsValid(constrForm->conparentid)); - Assert(constrForm->confrelid == RelationGetRelid(partition)); - - /* prevent data changes into the referencing table until commit */ - rel = table_open(constrForm->conrelid, ShareLock); - - trig.tgoid = InvalidOid; - trig.tgname = NameStr(constrForm->conname); - trig.tgenabled = TRIGGER_FIRES_ON_ORIGIN; - trig.tgisinternal = true; - trig.tgconstrrelid = RelationGetRelid(partition); - trig.tgconstrindid = constrForm->conindid; - trig.tgconstraint = constrForm->oid; - trig.tgdeferrable = false; - trig.tginitdeferred = false; - /* we needn't fill in remaining fields */ - - RI_PartitionRemove_Check(&trig, rel, partition); - - ReleaseSysCache(tuple); - - table_close(rel, NoLock); - } -} - /* * resolve column compression specification to compression method. */ diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c index 8ba038c5ef4..453bbf46cef 100644 --- a/src/backend/partitioning/partbounds.c +++ b/src/backend/partitioning/partbounds.c @@ -20,6 +20,7 @@ #include "catalog/partition.h" #include "catalog/pg_inherits.h" #include "catalog/pg_type.h" +#include "commands/partcmds.h" #include "commands/tablecmds.h" #include "common/hashfn.h" #include "executor/executor.h" diff --git a/src/include/commands/partcmds.h b/src/include/commands/partcmds.h new file mode 100644 index 00000000000..743431909fb --- /dev/null +++ b/src/include/commands/partcmds.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------- + * + * partcmds.h + * prototypes for partcmds.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/partcmds.h + * + *------------------------------------------------------------------------- + */ +#ifndef PARTCMDS_H +#define PARTCMDS_H + +/* to avoid including other headers */ +typedef struct AlteredTableInfo AlteredTableInfo; +typedef struct ParseState ParseState; +typedef struct AlterTableUtilityContext AlterTableUtilityContext; +typedef struct ForeignKeyCacheInfo ForeignKeyCacheInfo; + +extern bool tryAttachPartitionForeignKey(List **wqueue, + ForeignKeyCacheInfo *fk, + Relation partition, + Oid parentConstrOid, int numfks, + AttrNumber *mapped_conkey, AttrNumber *confkey, + Oid *conpfeqop, + Oid parentInsTrigger, + Oid parentUpdTrigger, + Relation trigrel); +extern PartitionSpec *transformPartitionSpec(Relation rel, PartitionSpec *partspec); +extern void ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNumber *partattrs, + List **partexprs, Oid *partopclass, Oid *partcollation, + PartitionStrategy strategy); +extern void CloneRowTriggersToPartition(Relation parent, Relation partition); +extern void CloneForeignKeyConstraints(List **wqueue, Relation parentRel, + Relation partitionRel); +extern bool PartConstraintImpliedByRelConstraint(Relation scanrel, + List *partConstraint); +extern bool ConstraintImpliedByRelConstraint(Relation scanrel, + List *testConstraint, List *provenConstraint); +extern ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel, + PartitionCmd *cmd, + AlterTableUtilityContext *context); +extern ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, + Relation rel, RangeVar *name, + bool concurrent); +extern ObjectAddress ATExecDetachPartitionFinalize(Relation rel, RangeVar *name); +extern ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, + RangeVar *name); + +#endif /* PARTCMDS_H */ diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index e9b0fab0767..35d33fa9339 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -24,6 +24,138 @@ typedef struct AlterTableUtilityContext AlterTableUtilityContext; /* avoid including * tcop/utility.h here */ +/* + * State information for ALTER TABLE + * + * The pending-work queue for an ALTER TABLE is a List of AlteredTableInfo + * structs, one for each table modified by the operation (the named table + * plus any child tables that are affected). We save lists of subcommands + * to apply to this table (possibly modified by parse transformation steps); + * these lists will be executed in Phase 2. If a Phase 3 step is needed, + * necessary information is stored in the constraints and newvals lists. + * + * Phase 2 is divided into multiple passes; subcommands are executed in + * a pass determined by subcommand type. + */ + +typedef enum AlterTablePass +{ + AT_PASS_UNSET = -1, /* UNSET will cause ERROR */ + AT_PASS_DROP, /* DROP (all flavors) */ + AT_PASS_ALTER_TYPE, /* ALTER COLUMN TYPE */ + AT_PASS_ADD_COL, /* ADD COLUMN */ + AT_PASS_SET_EXPRESSION, /* ALTER SET EXPRESSION */ + AT_PASS_OLD_INDEX, /* re-add existing indexes */ + AT_PASS_OLD_CONSTR, /* re-add existing constraints */ + /* We could support a RENAME COLUMN pass here, but not currently used */ + AT_PASS_ADD_CONSTR, /* ADD constraints (initial examination) */ + AT_PASS_COL_ATTRS, /* set column attributes, eg NOT NULL */ + AT_PASS_ADD_INDEXCONSTR, /* ADD index-based constraints */ + AT_PASS_ADD_INDEX, /* ADD indexes */ + AT_PASS_ADD_OTHERCONSTR, /* ADD other constraints, defaults */ + AT_PASS_MISC, /* other stuff */ +} AlterTablePass; + +#define AT_NUM_PASSES (AT_PASS_MISC + 1) + +typedef struct AlteredTableInfo +{ + /* Information saved before any work commences: */ + Oid relid; /* Relation to work on */ + char relkind; /* Its relkind */ + TupleDesc oldDesc; /* Pre-modification tuple descriptor */ + + /* + * Transiently set during Phase 2, normally set to NULL. + * + * ATRewriteCatalogs sets this when it starts, and closes when ATExecCmd + * returns control. This can be exploited by ATExecCmd subroutines to + * close/reopen across transaction boundaries. + */ + Relation rel; + + /* Information saved by Phase 1 for Phase 2: */ + List *subcmds[AT_NUM_PASSES]; /* Lists of AlterTableCmd */ + /* Information saved by Phases 1/2 for Phase 3: */ + List *constraints; /* List of NewConstraint */ + List *newvals; /* List of NewColumnValue */ + List *afterStmts; /* List of utility command parsetrees */ + bool verify_new_notnull; /* T if we should recheck NOT NULL */ + int rewrite; /* Reason for forced rewrite, if any */ + bool chgAccessMethod; /* T if SET ACCESS METHOD is used */ + Oid newAccessMethod; /* new access method; 0 means no change, + * if above is true */ + Oid newTableSpace; /* new tablespace; 0 means no change */ + bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */ + char newrelpersistence; /* if above is true */ + Expr *partition_constraint; /* for attach partition validation */ + /* true, if validating default due to some other attach/detach */ + bool validate_default; + /* Objects to rebuild after completing ALTER TYPE operations */ + List *changedConstraintOids; /* OIDs of constraints to rebuild */ + List *changedConstraintDefs; /* string definitions of same */ + List *changedIndexOids; /* OIDs of indexes to rebuild */ + List *changedIndexDefs; /* string definitions of same */ + char *replicaIdentityIndex; /* index to reset as REPLICA IDENTITY */ + char *clusterOnIndex; /* index to use for CLUSTER */ + List *changedStatisticsOids; /* OIDs of statistics to rebuild */ + List *changedStatisticsDefs; /* string definitions of same */ +} AlteredTableInfo; + +/* Alter table target-type flags for ATSimplePermissions */ +#define ATT_TABLE 0x0001 +#define ATT_VIEW 0x0002 +#define ATT_MATVIEW 0x0004 +#define ATT_INDEX 0x0008 +#define ATT_COMPOSITE_TYPE 0x0010 +#define ATT_FOREIGN_TABLE 0x0020 +#define ATT_PARTITIONED_INDEX 0x0040 +#define ATT_SEQUENCE 0x0080 +#define ATT_PARTITIONED_TABLE 0x0100 + +/* Partial or complete FK creation in addFkConstraint() */ +typedef enum addFkConstraintSides +{ + addFkReferencedSide, + addFkReferencingSide, + addFkBothSides, +} addFkConstraintSides; + +extern AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel); +extern void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets); +extern void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition); +extern void RemoveInheritance(Relation child_rel, Relation parent_rel, + bool expect_detached); +extern void addFkRecurseReferenced(Constraint *fkconstraint, + Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, + int numfks, int16 *pkattnum, int16 *fkattnum, + Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, + int numfkdelsetcols, int16 *fkdelsetcols, + bool old_check_ok, + Oid parentDelTrigger, Oid parentUpdTrigger, + bool with_period); +extern ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode, + bool recurse, bool recursing); +extern void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, + Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode); +extern ObjectAddress addFkConstraint(addFkConstraintSides fkside, + char *constraintname, + Constraint *fkconstraint, Relation rel, + Relation pkrel, Oid indexOid, + Oid parentConstr, + int numfks, int16 *pkattnum, int16 *fkattnum, + Oid *pfeqoperators, Oid *ppeqoperators, + Oid *ffeqoperators, int numfkdelsetcols, + int16 *fkdelsetcols, bool is_internal, + bool with_period); +extern 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, + int numfkdelsetcols, int16 *fkdelsetcols, + bool old_check_ok, LOCKMODE lockmode, + Oid parentInsTrigger, Oid parentUpdTrigger, + bool with_period); extern ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, ObjectAddress *typaddress, const char *queryString); @@ -105,7 +237,5 @@ extern void RangeVarCallbackMaintainsTable(const RangeVar *relation, extern void RangeVarCallbackOwnsRelation(const RangeVar *relation, Oid relId, Oid oldRelId, void *arg); -extern bool PartConstraintImpliedByRelConstraint(Relation scanrel, - List *partConstraint); #endif /* TABLECMDS_H */ -- 2.39.5 (Apple Git-154)